mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-09 16:22:52 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b71c0d8b93 | ||
|
|
0ecfe499fc | ||
|
|
68d85841ea | ||
|
|
1ffcc177a4 | ||
|
|
2230c5bc0c | ||
|
|
352b009e91 | ||
|
|
b6b33e1b1e | ||
|
|
dff1ac2e41 | ||
|
|
92601e0eb8 | ||
|
|
6e82423947 | ||
|
|
0a805a01f3 | ||
|
|
579b5a2f2f | ||
|
|
c77f43be5e | ||
|
|
f10d1834dd | ||
|
|
b4c7ed2227 | ||
|
|
0086980b1b | ||
|
|
89e1793978 | ||
|
|
5a598fe611 | ||
|
|
b58f670694 | ||
|
|
5103436d17 | ||
|
|
c9a1796e56 | ||
|
|
7428b56dd5 | ||
|
|
42294d261d | ||
|
|
31a99a908a | ||
|
|
aa503f0fe8 | ||
|
|
d3d48d27df | ||
|
|
bd8eb74662 | ||
|
|
99680e9a0f | ||
|
|
0cc58bdb46 | ||
|
|
09435b8786 | ||
|
|
5d2753a34e | ||
|
|
8eb3ece2f9 | ||
|
|
d3584a5ae9 | ||
|
|
c0c64621e6 | ||
|
|
323d364572 | ||
|
|
9a681a9422 | ||
|
|
a7a709df51 | ||
|
|
3f23b5553a | ||
|
|
5a2082ede5 | ||
|
|
e29f3a0a9a | ||
|
|
f4bebf6a41 | ||
|
|
564df8136c | ||
|
|
e85560952f | ||
|
|
5d6a4adae8 | ||
|
|
ecfd3204ff | ||
|
|
f0e6554309 | ||
|
|
995294e605 | ||
|
|
0525493c99 | ||
|
|
df2b56c985 | ||
|
|
e168305e64 | ||
|
|
c3b6d5c0ca | ||
|
|
60a1aaa3ab | ||
|
|
f1d14ce6e2 | ||
|
|
c246d522f9 | ||
|
|
ba4ed12ec8 | ||
|
|
8d89da6270 | ||
|
|
b8667bbe30 | ||
|
|
331da61f5a | ||
|
|
4251b0ce59 | ||
|
|
43b41e5f1f | ||
|
|
e12eb23cae | ||
|
|
041dbeda46 | ||
|
|
9a0cb32902 | ||
|
|
1c783ee6d1 | ||
|
|
d82e5585f5 | ||
|
|
847b24557a | ||
|
|
d7b91da8fd | ||
|
|
a5ab2e4080 | ||
|
|
eb6df45b00 | ||
|
|
873d1fac94 | ||
|
|
b5f0e0fdcb | ||
|
|
cf42836c95 | ||
|
|
75861d7172 | ||
|
|
4dfe91aedb | ||
|
|
6b6dd6fbf6 | ||
|
|
774b3fdf08 | ||
|
|
a1d93936f3 | ||
|
|
778deb9674 | ||
|
|
20e1954f0c | ||
|
|
b894ebb3c5 | ||
|
|
db3df7e6c9 | ||
|
|
658ae4a587 | ||
|
|
1e873d7e97 | ||
|
|
cd77bb827b | ||
|
|
35230a0674 | ||
|
|
8be316d87c | ||
|
|
c1d18a278d | ||
|
|
826c6b1036 | ||
|
|
e1eab37502 | ||
|
|
56a9c62bcb | ||
|
|
a25ebdccfb | ||
|
|
8940a14666 | ||
|
|
4e7d197c7f | ||
|
|
7dc6c98e56 | ||
|
|
5bbb9aa5e6 | ||
|
|
9a0713cedb | ||
|
|
43d37b875f | ||
|
|
33baf66371 | ||
|
|
5693e509b4 | ||
|
|
479a9548c1 | ||
|
|
f1e7078902 | ||
|
|
9b9d728ce3 | ||
|
|
5bf465b883 | ||
|
|
fd8fb4529c | ||
|
|
e05699e7ba | ||
|
|
147f62d87a | ||
|
|
c9a6125d92 | ||
|
|
30a3421710 | ||
|
|
a7f2329fee | ||
|
|
1a56e24def | ||
|
|
73a55e81b8 | ||
|
|
ac91e17f86 | ||
|
|
69405d4f0d | ||
|
|
aff82e4755 | ||
|
|
a8476e89f4 | ||
|
|
3ac15d880f | ||
|
|
8ead17e25e | ||
|
|
9e943a9681 | ||
|
|
b89dcfde65 | ||
|
|
c166e36c4e | ||
|
|
2c9c4d32db | ||
|
|
06ae4e97a0 | ||
|
|
e2097fdb31 | ||
|
|
ef08a50673 | ||
|
|
97e405ee8c | ||
|
|
55eed7c378 | ||
|
|
85f5dcf2cb | ||
|
|
a4c9990df1 | ||
|
|
72b764cc51 | ||
|
|
7e9bd432ce | ||
|
|
4b80e2b39b | ||
|
|
0ac947099c | ||
|
|
37a086d58d | ||
|
|
130cf51cc6 | ||
|
|
7e4a1e453b | ||
|
|
9c946b69e1 | ||
|
|
61ad94c0a0 | ||
|
|
56de4c63a7 | ||
|
|
1dfa31c275 | ||
|
|
e16087eb54 | ||
|
|
289293f23b | ||
|
|
59112c1a83 | ||
|
|
dd30ed460d | ||
|
|
bc54b5abaf | ||
|
|
05d316615a | ||
|
|
0eb8702a8e | ||
|
|
dfc6a6f93a | ||
|
|
1bb0b3c9bf | ||
|
|
89d5649b4f | ||
|
|
4b3b8d2795 | ||
|
|
eeff731858 | ||
|
|
63abd73942 | ||
|
|
c74bec2d29 | ||
|
|
bd74d21c83 | ||
|
|
41aee3d6bb | ||
|
|
396e75ed2d | ||
|
|
769e9c1730 | ||
|
|
33d38f33e5 | ||
|
|
876b6402c4 | ||
|
|
0e334a8f92 | ||
|
|
6218734783 | ||
|
|
da5ac9757a | ||
|
|
36107c11d5 | ||
|
|
5adf684dbe | ||
|
|
6bc3adbd33 | ||
|
|
c445a0732f | ||
|
|
20baed87ff | ||
|
|
ef9a2015eb | ||
|
|
583c71eca3 | ||
|
|
1e3c3c8f6c | ||
|
|
0a36166317 | ||
|
|
661f01319c | ||
|
|
6f8941d7be | ||
|
|
f7116edc9a | ||
|
|
ea69d56ab4 | ||
|
|
223e129477 | ||
|
|
4a5674a679 | ||
|
|
91c3a81cb1 | ||
|
|
e6921730c0 | ||
|
|
89ef0636a8 | ||
|
|
769ef001ec | ||
|
|
44b8ac79da | ||
|
|
5ada65b2a0 | ||
|
|
e433e7eda1 | ||
|
|
7de7dea1da | ||
|
|
86fb9bbd6c | ||
|
|
159474cf9b | ||
|
|
a2e7d4bdd1 | ||
|
|
b143a942ae | ||
|
|
c446dc263a | ||
|
|
60f718c2ce | ||
|
|
05df81be2f | ||
|
|
92cd62f0e9 | ||
|
|
602dfbabc2 | ||
|
|
c492a4122c | ||
|
|
fd55e84274 | ||
|
|
dba264cc5f | ||
|
|
7ff27a42ca | ||
|
|
8e1aa18b8f | ||
|
|
99cab7304d | ||
|
|
b5a9d52592 | ||
|
|
ca36938a83 | ||
|
|
de9d77a8ef | ||
|
|
0c3ebfbbb3 | ||
|
|
c575b861ed | ||
|
|
8323649c10 | ||
|
|
0dd84fad78 | ||
|
|
7dc25b3d33 | ||
|
|
5599e736ba | ||
|
|
e4e9a6b493 | ||
|
|
c50ee144df | ||
|
|
f5f32d902b | ||
|
|
f993d371a6 | ||
|
|
b5fb769bff | ||
|
|
28cbf6d803 | ||
|
|
89ddd937b0 | ||
|
|
5da950f61b | ||
|
|
7bff1b6c3c | ||
|
|
2baadd19cf | ||
|
|
2852aadc71 | ||
|
|
32e1854e13 | ||
|
|
b31daaa657 | ||
|
|
2401fc46ba | ||
|
|
0957d2d957 | ||
|
|
f8df9c502b | ||
|
|
52f071d8b0 | ||
|
|
a175ec5b5c | ||
|
|
f32ee0a9be | ||
|
|
f93702b82c | ||
|
|
0d4b006537 | ||
|
|
3f8477a442 | ||
|
|
bc9143b2a2 | ||
|
|
16c553196c | ||
|
|
b063ba412f | ||
|
|
aa406f191a | ||
|
|
f63b0cab81 | ||
|
|
beacc6ed1e | ||
|
|
028cfaa44c | ||
|
|
ac2949cf88 | ||
|
|
a098fae0f8 | ||
|
|
960e1f473f | ||
|
|
ffd72ed6f7 | ||
|
|
5eeab858d1 | ||
|
|
f64045168d | ||
|
|
f16d977e9b | ||
|
|
8274a69236 | ||
|
|
ce9f48e3d5 | ||
|
|
69735bdb22 | ||
|
|
f3e2143b4b | ||
|
|
2110eb9855 | ||
|
|
3c015990c5 | ||
|
|
0d3026b97d | ||
|
|
9cb8379c2d | ||
|
|
33c8ea7e2b | ||
|
|
561bed9fed | ||
|
|
7bdfa6a59d | ||
|
|
b887a941bf | ||
|
|
b76f5250a8 | ||
|
|
e372369b9e | ||
|
|
aaa0ea23e2 | ||
|
|
f8fe2cd532 | ||
|
|
bb362325ad | ||
|
|
56555ad550 | ||
|
|
456b441210 | ||
|
|
982594a4a5 | ||
|
|
eaded8dfd9 | ||
|
|
c583b95ee2 | ||
|
|
cda57dd733 | ||
|
|
60fbfce540 | ||
|
|
d8af2eaf4c | ||
|
|
9a571988ae | ||
|
|
b5b81a8bba | ||
|
|
e76551b4a8 | ||
|
|
74e071b1d7 | ||
|
|
327585327f | ||
|
|
4b445ca4df | ||
|
|
ef4a844233 | ||
|
|
80515658b0 | ||
|
|
e7d7dc6b6b | ||
|
|
aae0476967 | ||
|
|
aa6cf97324 | ||
|
|
8506671c5c | ||
|
|
022976d2f6 | ||
|
|
18d53167d0 | ||
|
|
9519bfddd4 | ||
|
|
0c556e32fa | ||
|
|
f9a02d9b3b | ||
|
|
3b6c986704 | ||
|
|
c78e8764a0 | ||
|
|
7f4d10c85d | ||
|
|
6b30ed0ba6 | ||
|
|
912d5c4d78 | ||
|
|
faa4bd82ae | ||
|
|
409f6b8dd5 | ||
|
|
4e79698b4e | ||
|
|
07b28605b5 | ||
|
|
c4403cc4de | ||
|
|
7e0742f29b | ||
|
|
b21aef221e | ||
|
|
20934f3ac8 | ||
|
|
715e497789 | ||
|
|
c0d389c283 | ||
|
|
6720575bd9 | ||
|
|
b1ec750d1d | ||
|
|
d9c78da997 | ||
|
|
1fa26e763d | ||
|
|
bb71b3d3d9 | ||
|
|
65e8e56e4a | ||
|
|
ee415d0e50 | ||
|
|
9db88a6c4f | ||
|
|
e5151a0a19 | ||
|
|
570234717a | ||
|
|
3f60d1f8de | ||
|
|
65d100c814 | ||
|
|
61bdab8621 | ||
|
|
8c94c9ecbf | ||
|
|
7fd895ecb5 | ||
|
|
213bc07646 | ||
|
|
1f081c181c | ||
|
|
db1d31900e | ||
|
|
2219c79ac3 | ||
|
|
d2782179aa | ||
|
|
d90a31afc7 | ||
|
|
1c236fe0f3 | ||
|
|
24165fb5f3 | ||
|
|
71da960386 | ||
|
|
a16c2d0a26 | ||
|
|
53de44a225 | ||
|
|
e32cff60e5 | ||
|
|
0ebba8d17c | ||
|
|
464db79c04 | ||
|
|
65019d8323 | ||
|
|
f131d316ca | ||
|
|
e6663c96b5 | ||
|
|
5f06d11fe9 | ||
|
|
6db2836662 | ||
|
|
cb209f8d98 | ||
|
|
5634e17a2e | ||
|
|
7c990edeaf | ||
|
|
9149f62760 | ||
|
|
872ec6b11c | ||
|
|
064a21a07c | ||
|
|
57c3c7b2e0 | ||
|
|
6dc75d7a44 | ||
|
|
5f3b7e05c5 | ||
|
|
61d3d8ad00 | ||
|
|
e36440f834 | ||
|
|
4029543d51 | ||
|
|
8aee9ac91f | ||
|
|
6e5e15f06f | ||
|
|
a72bd4cb7c | ||
|
|
d062ab553c | ||
|
|
a41edde12a | ||
|
|
7608927b4f | ||
|
|
24d7647c08 | ||
|
|
a14369cb7d | ||
|
|
3e81c4f3b6 | ||
|
|
84a6c19677 | ||
|
|
624a1d5509 | ||
|
|
8b6d35cfe2 | ||
|
|
867e502448 | ||
|
|
74275b969e | ||
|
|
275dde0630 | ||
|
|
46b470f30b | ||
|
|
fbfec95a8a | ||
|
|
1e75a18f55 | ||
|
|
a98849bf30 | ||
|
|
1b998e9d01 | ||
|
|
ebd6d3f3f9 | ||
|
|
1204afadbb | ||
|
|
cd08b72df7 | ||
|
|
c5ad7c98e2 | ||
|
|
4b8222d792 | ||
|
|
a3524ef8aa | ||
|
|
49ab8aa67d | ||
|
|
fa85639efb | ||
|
|
84bca81a9a | ||
|
|
206ea72a27 | ||
|
|
cc2aceb3d8 | ||
|
|
00b95aa6c3 | ||
|
|
a7632cc1e5 | ||
|
|
39f07025c6 | ||
|
|
658cb0403f | ||
|
|
db1519bcea | ||
|
|
885d1cc77c | ||
|
|
4005becd0a | ||
|
|
4c16b53c2a | ||
|
|
93dbf13d88 | ||
|
|
c5dd1a9002 | ||
|
|
18af7d6392 | ||
|
|
dbeb31d4fe | ||
|
|
0fb9234ebc | ||
|
|
6e4cec829a | ||
|
|
5a433c42e7 | ||
|
|
f808dd131b | ||
|
|
1bbf9a2318 | ||
|
|
ccff70d9c4 | ||
|
|
0347ca1bcc | ||
|
|
23564c14f9 | ||
|
|
93fca13510 | ||
|
|
1a39066cfc | ||
|
|
70b1574aa6 | ||
|
|
898f814dce | ||
|
|
9d425d33ab | ||
|
|
c6b0f58074 | ||
|
|
78b3802f8f | ||
|
|
833b90ccff | ||
|
|
c27a381286 | ||
|
|
ad29b43836 | ||
|
|
5cd9c51410 | ||
|
|
6476d42371 | ||
|
|
a48d9006ad | ||
|
|
c5c2057859 | ||
|
|
1889ab4825 | ||
|
|
0c81560f2c | ||
|
|
26005d6584 | ||
|
|
b0b0bcbdb7 | ||
|
|
546099fa2d | ||
|
|
c53fde6dca | ||
|
|
c9aeefc67a | ||
|
|
37594d3384 | ||
|
|
00dc5d8c38 | ||
|
|
ff8f6b1d00 | ||
|
|
aa60ba1fb1 | ||
|
|
3b8341f249 | ||
|
|
a68bcb6ee1 | ||
|
|
24a52a294c | ||
|
|
03ea67b20b | ||
|
|
4f20003f9a | ||
|
|
a92d13652d |
@@ -1 +0,0 @@
|
||||
* text=auto
|
||||
+21
-16
@@ -1,13 +1,21 @@
|
||||
[Tt]est[Rr]esult*
|
||||
/cache
|
||||
/logs
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Compiled source #
|
||||
###################
|
||||
# coverage generated
|
||||
/cover-html/
|
||||
.coverage
|
||||
.coveralls.yml
|
||||
|
||||
# Compiled source
|
||||
*.pyc
|
||||
*.py~
|
||||
*.pyproj
|
||||
*.sln
|
||||
|
||||
# Headphones files #
|
||||
######################
|
||||
# Headphones files
|
||||
*.log
|
||||
*.db*
|
||||
*.db-journal
|
||||
@@ -16,27 +24,25 @@ version.lock
|
||||
logs/*
|
||||
cache/*
|
||||
|
||||
# HTTPS Cert/Key #
|
||||
##################
|
||||
# HTTPS Cert/Key
|
||||
*.crt
|
||||
*.key
|
||||
*.csr
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
# OS generated files
|
||||
.DS_Store?
|
||||
.DS_Store
|
||||
ehthumbs.db
|
||||
Icon?
|
||||
Thumbs.db
|
||||
|
||||
#Ignore files generated by PyCharm
|
||||
# Ignore files generated by PyCharm
|
||||
.idea/*
|
||||
|
||||
#Ignore files generated by vi
|
||||
# Ignore files generated by vi
|
||||
*.swp
|
||||
|
||||
#Ignore files build by Visual Studio
|
||||
# Ignore files build by Visual Studio
|
||||
*.obj
|
||||
*.exe
|
||||
*.pdb
|
||||
@@ -61,8 +67,7 @@ Thumbs.db
|
||||
obj/
|
||||
[Rr]elease*/
|
||||
_ReSharper*/
|
||||
[Tt]est[Rr]esult*
|
||||
/cache
|
||||
/logs
|
||||
.project
|
||||
.pydevproject
|
||||
.vscode
|
||||
|
||||
# Python virtual env
|
||||
venv
|
||||
@@ -1,17 +1,7 @@
|
||||
[pep8]
|
||||
# E111 indentation is not a multiple of four
|
||||
# E121 continuation line under-indented for hanging indent
|
||||
# E122 continuation line missing indentation or outdented
|
||||
# E124 closing bracket does not match visual indentation
|
||||
# E125 continuation line with same indent as next logical line
|
||||
# E126 continuation line over-indented for hanging indent
|
||||
# E127 continuation line over-indented for visual indent
|
||||
# E128 continuation line under-indented for visual indent
|
||||
# E261 at least two spaces before inline comment
|
||||
# E262 inline comment should start with '# '
|
||||
# E265 block comment should start with '# '
|
||||
# E302 expected 2 blank lines, found 1
|
||||
# E501 line too long (312 > 160 characters)
|
||||
# E502 the backslash is redundant between brackets
|
||||
ignore = E111,E121,E122,E123,E124,E125,E126,E127,E128,E261,E262,E265,E302,E501,E502
|
||||
max-line-length = 160
|
||||
ignore = E123,E126,E127,E128,E501
|
||||
max-line-length = 160
|
||||
|
||||
+14
-9
@@ -3,19 +3,24 @@
|
||||
|
||||
language: python
|
||||
|
||||
# Available Python versions:
|
||||
# http://about.travis-ci.org/docs/user/ci-environment/#Python-VM-images
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
pip: true
|
||||
directories:
|
||||
- lib
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
# pylint 1.4 does not run under python 2.6
|
||||
|
||||
install:
|
||||
- pip install pyOpenSSL
|
||||
- pip install pylint==1.3.1
|
||||
- pip install pyflakes
|
||||
- pip install pep8
|
||||
- pip install -r requirements-dev.txt
|
||||
|
||||
script:
|
||||
- pep8 headphones
|
||||
- pylint --rcfile=pylintrc headphones
|
||||
- pyflakes headphones
|
||||
- nosetests headphones
|
||||
- nosetests
|
||||
|
||||
after_success:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == "2.7" ]]; then coveralls; fi
|
||||
|
||||
@@ -66,7 +66,7 @@ Unmark album as wanted / i.e. mark as skipped
|
||||
|
||||
### forceSearch
|
||||
force search for wanted albums - not launched in a separate thread so it may take a bit to complete
|
||||
### forceProcess
|
||||
### forceProcess[&dir=/path/to/folder]
|
||||
Force post process albums in download directory - also not launched in a separate thread
|
||||
### forceActiveArtistsUpdate
|
||||
force Active Artist Update - also not launched in a separate thread
|
||||
@@ -108,4 +108,4 @@ See above.
|
||||
Gives you a list of results from searcher.searchforalbum(). Basically runs a normal search, but rather than sorting them and downloading the best result, it dumps the data, which you can then pass on to download_specific_release(). Returns a list of dictionaries with params: title, size, url, provider & kind - all of these values must be passed back to download_specific_release
|
||||
|
||||
### download_specific_release&id=albumid&title=$title&size=$size&url=$url&provider=$provider&kind=$kind
|
||||
Allows you to manually pass a choose_specific_download release back to searcher.send_to_downloader()
|
||||
Allows you to manually pass a choose_specific_download release back to searcher.send_to_downloader()
|
||||
|
||||
+198
-1
@@ -1,5 +1,202 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.18
|
||||
Released 01 December 2016
|
||||
|
||||
Highlights:
|
||||
* Added: PassTheHeadphones support
|
||||
* Fixed: Special characters in password fields breaking on config page
|
||||
* Improved: Updated t411 url
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.17...v0.5.18).
|
||||
|
||||
|
||||
## v0.5.17
|
||||
Released 10 November 2016
|
||||
|
||||
Highlights:
|
||||
* Added: t411 support
|
||||
* Fixed: Rutracker login
|
||||
* Fixed: Deluge empty password
|
||||
* Fixed: FreeBSD init script
|
||||
* Improved: Musicbrainz searching
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.16...v0.5.17).
|
||||
|
||||
## v0.5.16
|
||||
Released 10 June 2016
|
||||
|
||||
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
|
||||
|
||||
Hotfix update
|
||||
|
||||
Highlights:
|
||||
* Fixed: Update vip mirror url to point to new server
|
||||
* Fixed: Update waffles url to .ch
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.14...v0.5.15).
|
||||
|
||||
## v0.5.14
|
||||
Released 02 June 2016
|
||||
|
||||
Highlights:
|
||||
* Fixed: File/folder format on new installs
|
||||
* Fixed: Pep8 errors
|
||||
* Improved: Updated fontawesome
|
||||
* Improved: Reverted back to less
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.13...v0.5.14).
|
||||
|
||||
## v0.5.13
|
||||
Released 25 February 2016
|
||||
|
||||
Another hotfix update
|
||||
|
||||
Highlights:
|
||||
* Fixed: Saving config with non-defined options
|
||||
* Fixed: Pep8 errors
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.12...v0.5.13).
|
||||
|
||||
## v0.5.12
|
||||
Released 25 February 2016
|
||||
|
||||
This is mostly a hotfix update
|
||||
|
||||
Highlights:
|
||||
* Added: Experimental Deluge Support
|
||||
* Fixed: Some pep8 stuff
|
||||
* Improved: Use curly braces for pathrender optional variables
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.11...v0.5.12).
|
||||
|
||||
## v0.5.11
|
||||
Released 20 February 2016
|
||||
|
||||
Highlights:
|
||||
* Added: Soft chroot option
|
||||
* Fixed: Post processing temporary directory fix (#2504)
|
||||
* Fixed: Ubuntu init script (#2509)
|
||||
* Fixed: Image cache uncaught exception (#2485)
|
||||
* Improved: $Date/$date variable in folder renaming
|
||||
* Improved: Reuse transmission session id
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.10...v0.5.11).
|
||||
|
||||
## v0.5.10
|
||||
Released 29 January 2016
|
||||
|
||||
Highlights:
|
||||
* Added: API option to post-process single folders
|
||||
* Added: Ability to specify extension when re-encoding
|
||||
* Added: Option to stop renaming folders
|
||||
* Fixed: Utorrent torrents not being removed (#2385)
|
||||
* Fixed: Torznab to transmission
|
||||
* Fixed: Magnet folder names in history
|
||||
* Fixed: Multiple torcache fixes
|
||||
* Fixed: Updated requests & urllib3 to latest versions to fix errors with pyOpenSSL
|
||||
* Improved: Use a temporary folder during post-processing
|
||||
* Improved: Added verify_ssl_cert option
|
||||
* Improved: Fixed track matching progress
|
||||
* Improved: pylint, pep8 & pylint fixes
|
||||
* Improved: Stop JS links from scrolling to the top of the page
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.9...v0.5.10).
|
||||
|
||||
## v0.5.9
|
||||
Released 05 September 2015
|
||||
|
||||
Highlights:
|
||||
* Added: Providers Strike, Jackett, custom Torznabs
|
||||
* Added: Option to stop post-processing if no good match found (#2343)
|
||||
* Fixed: Blackhole -> Magnet, limit to torcache
|
||||
* Fixed: Kat 403 flac error
|
||||
* Fixed: Last.fm errors
|
||||
* Fixed: Pushover notifications
|
||||
* Improved: Rutracker logging, switched to requests lib
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.8...v0.5.9).
|
||||
|
||||
## v0.5.8
|
||||
Released 13 July 2015
|
||||
|
||||
Highlights:
|
||||
* Added: Option to only include official extras
|
||||
* Added: Option to wait until album release date before searching
|
||||
* Fixed: NotifyMyAndroid notifications
|
||||
* Fixed: Plex Notifications
|
||||
* Fixed: Metacritic parsing
|
||||
* Fixed: Pushbullet notifications
|
||||
* Fixed: What.cd not honoring custom search term (#2279)
|
||||
* Improved: XSS Search bug
|
||||
* Improved: Config page layout
|
||||
* Improved: Set localhost as default
|
||||
* Improved: Better single artist scanning
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.7...v0.5.8).
|
||||
|
||||
## v0.5.7
|
||||
Released 01 July 2015
|
||||
|
||||
Highlights:
|
||||
* Improved: Moved pushover to use requests lib
|
||||
* Improved: Plex tokens with Plex Home
|
||||
* Improved: Added getLogs & clearLogs to api
|
||||
* Improved: Cache MetaCritic scores. Added user-agent header to fix 403 errors
|
||||
* Improved: Specify whether to delete folders when force post-processing
|
||||
* Improved: Convert target bitrate to vbr preset for what.cd searching
|
||||
* Improved: Switched Pushover to requests lib
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.6...v0.5.7).
|
||||
|
||||
## v0.5.6
|
||||
Released 08 June 2015
|
||||
|
||||
Highlights:
|
||||
* Added: Metacritic scores
|
||||
* Added: Series support (e.g. Cafe Del Mar, Now That's What I Call Music, etc)
|
||||
* Added: Filter out clean/edited/censored releases (#2198)
|
||||
* Added: Button on the log page to toggle verbose/debug logging
|
||||
* Fixed: Connecting to SABnzbd over https with python >= 2.7.9
|
||||
* Fixed: Email Notifications with SSL
|
||||
* Fixed: Don't limit musicbrainz results to first 100
|
||||
* Fixed: nzbget url fix
|
||||
* Fixed: OSX Notifications
|
||||
* Improved: Cuesplit, allow wav, ape to be split
|
||||
* Improved: Moved the 'freeze db' option to the advanced->misc. tab
|
||||
* Improved: Moved kickass searching to json api, so it doesn't throw 404 errors anymore when there are no results
|
||||
* Improved: SSL for headphones indexer
|
||||
* Improved: Disable update dialog box if check_github is diabled
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.5...v0.5.6).
|
||||
|
||||
## v0.5.5
|
||||
Released 04 May 2015
|
||||
|
||||
Highlights:
|
||||
* Added: force ID3v2.3 during post processing (#2121)
|
||||
* Added: MusicBrainz authentication (#2125)
|
||||
* Added: Email notifications (addresses #1045)
|
||||
* Fixed: Kickass url updated to kickass.to (#2119)
|
||||
* Fixed: Piratebay returning 0 bytes for all files
|
||||
* Fixed: Albums stopped automatically refreshing when adding an artist
|
||||
* Fixed: Min/max sizes for target bitrate
|
||||
* Fixed: Don't filter any results if looking for a specific download
|
||||
* Fixed: Sort by size in the specific download table
|
||||
* Fixed: Deal with beets recommendation.none correctly
|
||||
* Improved: Close dialog window automatically when choosing a specific download
|
||||
* Improved: Move some repetitive log messages to debug level
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.4...v0.5.5).
|
||||
|
||||
## v0.5.4
|
||||
Released 05 February 2015
|
||||
|
||||
@@ -155,4 +352,4 @@ The full list of commits can be found [here](https://github.com/rembo10/headphon
|
||||
## v0.1
|
||||
Released 05 August 2013
|
||||
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
|
||||
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/2156e1341405d07c5bcfbe994f6b354b32d94cda...v0.1).
|
||||
|
||||
+14
-2
@@ -81,6 +81,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()
|
||||
|
||||
@@ -152,7 +154,10 @@ def main():
|
||||
headphones.DB_FILE = os.path.join(headphones.DATA_DIR, 'headphones.db')
|
||||
|
||||
# Read config and start logging
|
||||
headphones.initialize(config_file)
|
||||
try:
|
||||
headphones.initialize(config_file)
|
||||
except headphones.exceptions.SoftChrootError as e:
|
||||
raise SystemExit('FATAL ERROR')
|
||||
|
||||
if headphones.DAEMON:
|
||||
headphones.daemonize()
|
||||
@@ -167,6 +172,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:
|
||||
@@ -180,7 +192,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,6 +1,9 @@
|
||||
# Headphones
|
||||
## Headphones
|
||||
|
||||
Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent and Blackhole.
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
[](https://travis-ci.org/rembo10/headphones)
|
||||
|
||||
Headphones is an automated music downloader for NZB and Torrent, written in Python. It supports SABnzbd, NZBget, Transmission, µTorrent, Deluge and Blackhole.
|
||||
|
||||
## Support & Discuss
|
||||
You are free to join the Headphones support community on IRC where you can ask questions, hang around and discuss anything related to HP.
|
||||
@@ -18,8 +21,8 @@ You are free to join the Headphones support community on IRC where you can ask q
|
||||
|
||||
1. Analyze your log, you just might find the solution yourself!
|
||||
2. You read the wiki and searched existing issues, but this is not solving your problem.
|
||||
3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
|
||||
4. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
|
||||
3. Post the issue with a clear title, description and the HP log and use [proper markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your text (code/log in code blocks).
|
||||
4. Close your issue when it's solved! If you found the solution yourself, please comment so that others benefit from it.
|
||||
|
||||
**Feature requests** can be reported on the GitHub issue tracker too:
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Ensure that we use the Headphones provided libraries.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../lib"))
|
||||
|
||||
import urlparse
|
||||
|
||||
|
||||
def can_import(module):
|
||||
"""
|
||||
Return True if a given module can be imported or not.
|
||||
"""
|
||||
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
# Module can be imported
|
||||
return True
|
||||
|
||||
|
||||
def check_installation():
|
||||
"""
|
||||
Check if some core modules are available. Info is based on this topic:
|
||||
https://github.com/rembo10/headphones/issues/2210.
|
||||
"""
|
||||
|
||||
if can_import("requests"):
|
||||
import requests
|
||||
requests_version = requests.__version__
|
||||
else:
|
||||
requests_version = "no"
|
||||
|
||||
if can_import("OpenSSL"):
|
||||
import OpenSSL
|
||||
openssl_version = OpenSSL.__version__
|
||||
else:
|
||||
openssl_version = "no"
|
||||
|
||||
if can_import("cryptography"):
|
||||
import cryptography
|
||||
cryptography_version = cryptography.__version__
|
||||
else:
|
||||
cryptography_version = "no"
|
||||
|
||||
if can_import("pyasn1"):
|
||||
import pyasn1
|
||||
pyasn1_version = pyasn1.__version__
|
||||
else:
|
||||
pyasn1_version = "no"
|
||||
|
||||
if can_import("ndg.httpsclient"):
|
||||
from ndg import httpsclient
|
||||
ndg_version = httpsclient.__date__
|
||||
else:
|
||||
ndg_version = "no"
|
||||
|
||||
# Print some system information.
|
||||
sys.stdout.write(
|
||||
"* Checking Python version: %s.%s.%s\n" % sys.version_info[:3])
|
||||
sys.stdout.write("* Operating system: %s\n" % sys.platform)
|
||||
|
||||
sys.stdout.write(
|
||||
"* Checking if requests can be imported: %s\n" % requests_version)
|
||||
sys.stdout.write(
|
||||
"* Checking if pyOpenSSL is installed: %s\n" % openssl_version)
|
||||
sys.stdout.write(
|
||||
"* Checking if cryptography is installed: %s\n" % cryptography_version)
|
||||
sys.stdout.write(
|
||||
"* Checking if pyasn1 is installed: %s\n" % pyasn1_version)
|
||||
sys.stdout.write(
|
||||
"* Checking if ndg.httpsclient is installed: %s\n" % ndg_version)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Test if the current Headphones installation can connect to SNI-enabled
|
||||
servers.
|
||||
"""
|
||||
|
||||
# Read the URL to test.
|
||||
if len(sys.argv) == 1:
|
||||
url = "https://sni.velox.ch/"
|
||||
else:
|
||||
url = sys.argv[1]
|
||||
|
||||
# Check if it is a HTTPS website.
|
||||
parts = urlparse.urlparse(url)
|
||||
|
||||
if parts.scheme.lower() != "https":
|
||||
sys.stderr.write(
|
||||
"Error: provided URL does not start with https://\n")
|
||||
return 1
|
||||
|
||||
# Gather information
|
||||
check_installation()
|
||||
|
||||
# Do the request.
|
||||
if not can_import("requests"):
|
||||
sys.stderr.exit("Error: cannot continue without requests module!\n")
|
||||
return 1
|
||||
|
||||
sys.stdout.write("* Performing request: %s\n" % url)
|
||||
|
||||
import requests
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
try:
|
||||
try:
|
||||
response = requests.get(url)
|
||||
except requests.exceptions.SSLError as e:
|
||||
sys.stdout.write(
|
||||
"- Server certificate seems invalid. I will disable "
|
||||
"certificate check and try again. You'll see the real "
|
||||
"exception if it fails again.\n")
|
||||
sys.stdout.write(
|
||||
"* Retrying request with certificate verification off.\n")
|
||||
response = requests.get(url)
|
||||
except Exception as e:
|
||||
sys.stdout.write(
|
||||
"- An error occured while performing the request. The "
|
||||
"exception was: %s\n" % e.message)
|
||||
sys.stdout.write(
|
||||
"- Consult the Troubleshooting wiki (https://github.com/"
|
||||
"rembo10/headphones/wiki/Troubleshooting) before you post an "
|
||||
"issue!")
|
||||
return 0
|
||||
|
||||
# Verify the response.
|
||||
if response.status_code == 200:
|
||||
sys.stdout.write("+ Got a valid response. All seems OK!\n")
|
||||
else:
|
||||
sys.stdout.write(
|
||||
"- Server returned status code %s. Expected a status code 200.\n",
|
||||
response.status_code)
|
||||
sys.stdout.write(
|
||||
"- However, I was able to communicate to the server!\n")
|
||||
|
||||
# E.g. `python sni_test.py https://example.org'.
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -14,15 +14,15 @@
|
||||
|
||||
<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="#" 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>
|
||||
<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="#" 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="#" 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>
|
||||
<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="#" 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="#" 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>
|
||||
<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="#"><i class="fa fa-pencil"></i> Choose Alternate Release</a>
|
||||
<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">
|
||||
<%
|
||||
@@ -30,11 +30,12 @@
|
||||
%>
|
||||
%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="#" data-success="'${album['ArtistName']}' is being refreshed">Refresh Artist</a></h2>
|
||||
<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]"
|
||||
@@ -42,12 +43,12 @@
|
||||
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="#" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><br>
|
||||
<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>
|
||||
</div>
|
||||
<a class="menu_link_edit" id="edit_search_term" href="#"><i class="fa fa-pencil"></i> Edit Search Term</a>
|
||||
<a class="menu_link_edit" id="edit_search_term" href="javascript:void(0)"><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">
|
||||
<input type="hidden" name="AlbumID" value="${album['AlbumID']}">
|
||||
@@ -63,7 +64,7 @@
|
||||
<input type="button" value="Save changes" onclick="doAjaxCall('editSearchTerm',$(this),'tabs',true);return false;" data-success="Search term updated"/>
|
||||
</form>
|
||||
</div>
|
||||
<a class="menu_link_edit" id="choose_specific_download" href="#" onclick="getAvailableDownloads()"><i class="fa fa-search"></i> Choose Specific Download</a>
|
||||
<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>
|
||||
<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>
|
||||
@@ -237,7 +238,7 @@
|
||||
feedback.fadeOut();
|
||||
search_results = data
|
||||
for( var i = 0, len = data.length; i < len; i++ ) {
|
||||
$('#downloads_table_body').append('<tr><td id="title"><a href="#" 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="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').dataTable({
|
||||
"aoColumns": [
|
||||
@@ -337,6 +338,21 @@
|
||||
});
|
||||
}
|
||||
|
||||
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
|
||||
"title-numeric-pre": function ( a ) {
|
||||
var x = a.match(/title="*(-?[0-9\.]+)/)[1];
|
||||
return parseFloat( x );
|
||||
},
|
||||
|
||||
"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();
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_refresh" onclick="doSimpleAjaxCall('refreshArtist?ArtistID=${artist['ArtistID']}')" href="#"><i class="fa fa-refresh"></i> Refresh Artist</a>
|
||||
<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_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="#" data-success="'${artist['ArtistName']}' was scanned"><i class="fa fa-refresh"></i> Scan 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>
|
||||
%if artist['Status'] == 'Paused':
|
||||
<a id="menu_link_resume" href="#" 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="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>
|
||||
%else:
|
||||
<a id="menu_link_pauze" href="#" 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="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>
|
||||
%endif
|
||||
%if artist['IncludeExtras']:
|
||||
<a id="menu_link_removeextra" href="#" 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="#"><i class="fa fa-pencil"></i> Modify Extras</a>
|
||||
<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>
|
||||
%else:
|
||||
<a id="menu_link_getextra" href="#"><i class="fa fa-plus"></i> Get Extras</a>
|
||||
<a id="menu_link_getextra" href="javascript:void(0)"><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">
|
||||
@@ -70,6 +70,7 @@
|
||||
<th id="albumname">Name</th>
|
||||
<th id="reldate">Date</th>
|
||||
<th id="type">Type</th>
|
||||
<th id="score">Metacritic</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="have">Have</th>
|
||||
<th id="bitrate">Bitrate</th>
|
||||
@@ -125,18 +126,19 @@
|
||||
<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="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' added to Wanted list">want</a>]
|
||||
[<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>]
|
||||
%elif (album['Status'] == 'Wanted' or album['Status'] == 'Wanted Lossless'):
|
||||
[<a href="#" onclick="doAjaxCall('unqueueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}',$(this),'table')" data-success="'${album['AlbumTitle']}' skipped">skip</a>] [<a href="#" 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="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>]
|
||||
%else:
|
||||
[<a href="#" 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="#" 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="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>]
|
||||
%endif
|
||||
%if albumformat in lossy_formats and album['Status'] == 'Skipped':
|
||||
[<a id="wantlossless" href="#" 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="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>]
|
||||
%elif albumformat in lossy_formats and (album['Status'] == 'Snatched' or album['Status'] == 'Downloaded'):
|
||||
[<a id="wantlossless" href="#" 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="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>]
|
||||
%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>
|
||||
@@ -218,7 +220,7 @@
|
||||
$("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 a").live("click", function() {
|
||||
$("li.more").on("click", 'a', function() {
|
||||
var li = $(this).parents("li:first");
|
||||
li.parent().children().show();
|
||||
li.remove();
|
||||
@@ -283,6 +285,7 @@
|
||||
{ "sType": "date" },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "sType": "title-numeric"},
|
||||
null,
|
||||
null
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
% if not headphones.CURRENT_VERSION:
|
||||
% 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="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||
<a href="javascript:void(0)" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||
</div>
|
||||
% elif headphones.CURRENT_VERSION != headphones.LATEST_VERSION and headphones.COMMITS_BEHIND > 0 and headphones.INSTALL_TYPE != 'win':
|
||||
% 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="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
||||
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
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<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>
|
||||
@@ -81,6 +82,9 @@
|
||||
<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>
|
||||
@@ -88,7 +92,7 @@
|
||||
<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="#" 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>
|
||||
<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">
|
||||
|
||||
+679
-307
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
# About less and css
|
||||
|
||||
**The main message of this document: DO NOT MODIFY style.css - IT IS GENERATED FILE**
|
||||
|
||||
----
|
||||
|
||||
If you want modify the appearance of Headphones, please, follow this simple steps.
|
||||
|
||||
0. **Install**. Do this just once, and if you still do not have `lessc` utility on your PC. Here is very useful guide, how to install less : [http://lesscss.org/#using-less-installation](http://lesscss.org/#using-less-installation)
|
||||
1. **Modify**. Carefully add your changes to the `style.less` (`.less` extension, not `.css`).
|
||||
2. **Compile**. Currently, there is no magic, so you should compile css manually. Go to the `/data/interfaces/default/css` folder (or use full paths..), and then just type:
|
||||
|
||||
```bash
|
||||
lessc style.less > style.css
|
||||
```
|
||||
|
||||
_works good on *nix hosts, I didn't test this on win-hosts_
|
||||
|
||||
DONE. You have new CSS file.
|
||||
|
||||
## LESS
|
||||
|
||||
Less is very useful tool (CSS pre-processor) for CSS writing. There is the awesome guide on the official site: [Official Less Guide](http://lesscss.org/features/)
|
||||
|
||||
Thanks!
|
||||
@@ -10,9 +10,16 @@
|
||||
@msg-bg: #FFF6A9;
|
||||
@msg-bg-success: #D3FFD7;
|
||||
@msg-bg-error: #FFD3D3;
|
||||
@brand-color: #4F4F4F;
|
||||
@caption-color: #999999;
|
||||
|
||||
@gradient-color-1: #FAFAFA;
|
||||
@gradient-color-2: #E7E7E7;
|
||||
|
||||
@heading-input-radius: 5px;
|
||||
|
||||
/* Mixins */
|
||||
.rounded(@radius: 5px) {
|
||||
.rounded(@radius: 5px) {
|
||||
-moz-border-radius: @radius;
|
||||
-webkit-border-radius: @radius;
|
||||
border-radius: @radius;
|
||||
@@ -75,3 +82,32 @@
|
||||
opacity:@opacity_percent / 100 !important;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
COLUMNS STUFF
|
||||
https://css-tricks.com/guide-responsive-friendly-css-columns/
|
||||
*/
|
||||
.columnWidth(@width) {
|
||||
-webkit-column-width: @width;
|
||||
-moz-column-width: @width;
|
||||
column-width: @width;
|
||||
}
|
||||
.columnCount(@count) {
|
||||
-webkit-columns: @count;
|
||||
-moz-columns: @count;
|
||||
columns: @count;
|
||||
}
|
||||
.columnGap(@width) {
|
||||
-webkit-column-gap: @width;
|
||||
-moz-column-gap: @width;
|
||||
column-gap: @width;
|
||||
}
|
||||
.columnRule(@border) {
|
||||
-webkit-column-rule: @border;
|
||||
-moz-column-rule: @border;
|
||||
column-rule: @border;
|
||||
}
|
||||
.columnSpan(@what) {
|
||||
-webkit-column-span: @what;
|
||||
column-span: @what;
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -1,5 +1,9 @@
|
||||
/* Variables */
|
||||
/* Mixins */
|
||||
/*
|
||||
COLUMNS STUFF
|
||||
https://css-tricks.com/guide-responsive-friendly-css-columns/
|
||||
*/
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
@@ -103,7 +107,7 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
a {
|
||||
color: #4183c4;
|
||||
color: #4183C4;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
@@ -125,7 +129,7 @@ a .ui-icon {
|
||||
float: left;
|
||||
}
|
||||
.links a:hover {
|
||||
color: #4183c4;
|
||||
color: #4183C4;
|
||||
}
|
||||
.links a .ui-icon {
|
||||
float: left;
|
||||
@@ -146,14 +150,106 @@ p.center {
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #cccccc;
|
||||
border-top: 1px solid #CCCCCC;
|
||||
display: block;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
small {
|
||||
font-size: 85%;
|
||||
small,
|
||||
.small {
|
||||
font-size: xx-small;
|
||||
}
|
||||
.configtable legend {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 1px 1px 0 #FFFFFF;
|
||||
}
|
||||
.configtable tr td:last-child {
|
||||
border-left: 1px dotted #ddd;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.configtable td {
|
||||
padding-right: 15px;
|
||||
width: 50%;
|
||||
}
|
||||
.config legend {
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
text-shadow: 1px 1px 0 #FFFFFF;
|
||||
}
|
||||
.config .caption {
|
||||
color: #999999;
|
||||
line-height: 12px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.config .tab {
|
||||
-webkit-column-rule: 1px solid #CCCCCC;
|
||||
-moz-column-rule: 1px solid #CCCCCC;
|
||||
column-rule: 1px solid #CCCCCC;
|
||||
-webkit-columns: 2;
|
||||
-moz-columns: 2;
|
||||
columns: 2;
|
||||
-webkit-column-gap: 4em;
|
||||
-moz-column-gap: 4em;
|
||||
column-gap: 4em;
|
||||
}
|
||||
.config .tab .message {
|
||||
-webkit-column-span: all;
|
||||
column-span: all;
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.config .block {
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid-column;
|
||||
}
|
||||
.config .option {
|
||||
margin-bottom: 1em;
|
||||
font-size: 12px;
|
||||
}
|
||||
.config .option:after {
|
||||
content: " ";
|
||||
/* Older browser do not support empty content */
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
.config .option .input-wrapper {
|
||||
float: left;
|
||||
}
|
||||
.config .option label {
|
||||
width: 170px;
|
||||
font-size: 12px;
|
||||
float: left;
|
||||
}
|
||||
.config .option input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.config .option input[type=text],
|
||||
.config .option input[type=password],
|
||||
.config .option input[type=number] {
|
||||
border: 1px solid #DDD;
|
||||
border-top: 1px solid #CDCDCD;
|
||||
-moz-box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
-webkit-box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
-o-box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
color: #343434;
|
||||
font-size: larger;
|
||||
max-width: 230px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.config .option input[type=number] {
|
||||
max-width: 5em;
|
||||
}
|
||||
.config .embed-option-block {
|
||||
margin-left: 1em;
|
||||
}
|
||||
img.albumArt {
|
||||
float: left;
|
||||
@@ -184,17 +280,17 @@ table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
table th {
|
||||
background-image: -moz-linear-gradient(#fafafa, #eaeaea) !important;
|
||||
background-image: linear-gradient(#fafafa, #eaeaea) !important;
|
||||
background-image: -webkit-linear-gradient(#fafafa, #eaeaea) !important;
|
||||
background-image: -o-linear-gradient(#fafafa, #eaeaea) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
|
||||
background-image: -moz-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: -webkit-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: -o-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
|
||||
border-left: 1px solid #E0E0E0;
|
||||
-moz-box-shadow: 1px 0 0 #fafafa;
|
||||
-webkit-box-shadow: 1px 0 0 #fafafa;
|
||||
-o-box-shadow: 1px 0 0 #fafafa;
|
||||
box-shadow: 1px 0 0 #fafafa;
|
||||
-moz-box-shadow: 1px 0 0 #FAFAFA;
|
||||
-webkit-box-shadow: 1px 0 0 #FAFAFA;
|
||||
-o-box-shadow: 1px 0 0 #FAFAFA;
|
||||
box-shadow: 1px 0 0 #FAFAFA;
|
||||
text-shadow: 1px 1px 0 #FFFFFF ;
|
||||
}
|
||||
table th input[type="checkbox"] {
|
||||
@@ -209,13 +305,13 @@ table th:first-child {
|
||||
}
|
||||
table th.sorting_desc,
|
||||
table th.sorting_asc {
|
||||
background-image: -moz-linear-gradient(#fafbfd, #dce6ef) !important;
|
||||
background-image: linear-gradient(#fafbfd, #dce6ef) !important;
|
||||
background-image: -webkit-linear-gradient(#fafbfd, #dce6ef) !important;
|
||||
background-image: -o-linear-gradient(#fafbfd, #dce6ef) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafbfd, endColorstr=#dce6ef) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafbfd, endColorstr=#dce6ef) !important;
|
||||
color: #4183c4;
|
||||
background-image: -moz-linear-gradient(#FAFBFD, #DCE6EF) !important;
|
||||
background-image: linear-gradient(#FAFBFD, #DCE6EF) !important;
|
||||
background-image: -webkit-linear-gradient(#FAFBFD, #DCE6EF) !important;
|
||||
background-image: -o-linear-gradient(#FAFBFD, #DCE6EF) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFBFD, endColorstr=#DCE6EF) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFBFD, endColorstr=#DCE6EF) !important;
|
||||
color: #4183C4;
|
||||
}
|
||||
table td {
|
||||
vertical-align: top;
|
||||
@@ -227,7 +323,7 @@ select,
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
font: 99%;
|
||||
font-size: 99%;
|
||||
}
|
||||
textarea {
|
||||
overflow: auto;
|
||||
@@ -275,8 +371,7 @@ input,
|
||||
select,
|
||||
form .checkbox input,
|
||||
.configtable td#middle,
|
||||
#artist_table td#have,
|
||||
#album_table td#have {
|
||||
#artist_table td#have {
|
||||
vertical-align: middle;
|
||||
}
|
||||
input[type="radio"] {
|
||||
@@ -325,7 +420,6 @@ form fieldset small.heading {
|
||||
margin-top: -15px;
|
||||
}
|
||||
form .row {
|
||||
font-family: Helvetica, Arial;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
form .row label {
|
||||
@@ -334,7 +428,7 @@ form .row label {
|
||||
font-size: 12px;
|
||||
line-height: normal;
|
||||
padding-top: 7px;
|
||||
width: 175px;
|
||||
width: 170px;
|
||||
}
|
||||
form .row label.inline {
|
||||
margin-right: 5px;
|
||||
@@ -368,7 +462,7 @@ form .row small {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
line-height: 12px;
|
||||
margin-left: 175px;
|
||||
margin-left: 170px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
form .left label {
|
||||
@@ -405,9 +499,6 @@ form .checkbox small {
|
||||
margin: 0 !important;
|
||||
width: auto;
|
||||
}
|
||||
form .indent input {
|
||||
margin-left: 15px;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 2em;
|
||||
@@ -450,24 +541,24 @@ ul#nav li a {
|
||||
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
ul#nav li a:hover {
|
||||
background-image: -moz-linear-gradient(#f1f1f1, #e0e0e0) !important;
|
||||
background-image: linear-gradient(#f1f1f1, #e0e0e0) !important;
|
||||
background-image: -webkit-linear-gradient(#f1f1f1, #e0e0e0) !important;
|
||||
background-image: -o-linear-gradient(#f1f1f1, #e0e0e0) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f1f1f1, endColorstr=#e0e0e0) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f1f1f1, endColorstr=#e0e0e0) !important;
|
||||
background-image: -moz-linear-gradient(#F1F1F1, #E0E0E0) !important;
|
||||
background-image: linear-gradient(#F1F1F1, #E0E0E0) !important;
|
||||
background-image: -webkit-linear-gradient(#F1F1F1, #E0E0E0) !important;
|
||||
background-image: -o-linear-gradient(#F1F1F1, #E0E0E0) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#F1F1F1, endColorstr=#E0E0E0) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#F1F1F1, endColorstr=#E0E0E0) !important;
|
||||
border: 1px solid #DDDDDD;
|
||||
-moz-border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-moz-box-shadow: 0 1px 0 #fafafa;
|
||||
-webkit-box-shadow: 0 1px 0 #fafafa;
|
||||
-o-box-shadow: 0 1px 0 #fafafa;
|
||||
box-shadow: 0 1px 0 #fafafa;
|
||||
-moz-box-shadow: 0 1px 0 #fafafa inset;
|
||||
-webkit-box-shadow: 0 1px 0 #fafafa inset;
|
||||
-o-box-shadow: 0 1px 0 #fafafa inset;
|
||||
box-shadow: 0 1px 0 #fafafa inset;
|
||||
-moz-box-shadow: 0 1px 0 #FAFAFA;
|
||||
-webkit-box-shadow: 0 1px 0 #FAFAFA;
|
||||
-o-box-shadow: 0 1px 0 #FAFAFA;
|
||||
box-shadow: 0 1px 0 #FAFAFA;
|
||||
-moz-box-shadow: 0 1px 0 #FAFAFA inset;
|
||||
-webkit-box-shadow: 0 1px 0 #FAFAFA inset;
|
||||
-o-box-shadow: 0 1px 0 #FAFAFA inset;
|
||||
box-shadow: 0 1px 0 #FAFAFA inset;
|
||||
-webkit-transition: color 0.2s ease-in;
|
||||
-moz-transition: color 0.2s ease-in;
|
||||
-o-transition: color 0.2s ease-in;
|
||||
@@ -487,13 +578,13 @@ ul#nav li a.log {
|
||||
padding: 10px 15px 11px;
|
||||
}
|
||||
header {
|
||||
background-image: -moz-linear-gradient(#fafafa, #eaeaea) !important;
|
||||
background-image: linear-gradient(#fafafa, #eaeaea) !important;
|
||||
background-image: -webkit-linear-gradient(#fafafa, #eaeaea) !important;
|
||||
background-image: -o-linear-gradient(#fafafa, #eaeaea) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fafafa, endColorstr=#eaeaea) !important;
|
||||
border-bottom: 1px solid #CACACA;
|
||||
background-image: -moz-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: -webkit-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: -o-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
-o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
@@ -533,12 +624,12 @@ footer {
|
||||
-moz-border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
background-image: -moz-linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
background-image: linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
|
||||
background-image: -moz-linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
background-image: linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
background-image: -webkit-linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
background-image: -o-linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
margin-top: 10px;
|
||||
@@ -550,23 +641,23 @@ footer {
|
||||
top: 4px;
|
||||
}
|
||||
.ajaxMsg {
|
||||
border: 1px solid #cccccc;
|
||||
background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: -webkit-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: -o-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
|
||||
border: 1px solid #CCCCCC;
|
||||
background-image: -moz-linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
background-image: linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
background-image: -webkit-linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
background-image: -o-linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
|
||||
-moz-border-radius: 7px;
|
||||
-webkit-border-radius: 7px;
|
||||
border-radius: 7px;
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
font-size: larger;
|
||||
right: 10px;
|
||||
-moz-box-shadow: 0px 0px 2px #aaaaaa;
|
||||
-webkit-box-shadow: 0px 0px 2px #aaaaaa;
|
||||
-o-box-shadow: 0px 0px 2px #aaaaaa;
|
||||
box-shadow: 0px 0px 2px #aaaaaa;
|
||||
-moz-box-shadow: 0px 0px 2px #aaa;
|
||||
-webkit-box-shadow: 0px 0px 2px #aaa;
|
||||
-o-box-shadow: 0px 0px 2px #aaa;
|
||||
box-shadow: 0px 0px 2px #aaa;
|
||||
padding: 7px 10px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
@@ -589,23 +680,23 @@ footer {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.ajaxMsg .success {
|
||||
background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: -o-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
|
||||
.ajaxMsg.success {
|
||||
background-image: -moz-linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
background-image: linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
background-image: -webkit-linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
background-image: -o-linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.ajaxMsg .error {
|
||||
background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: -o-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
|
||||
.ajaxMsg.error {
|
||||
background-image: -moz-linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
background-image: linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
background-image: -webkit-linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
background-image: -o-linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -617,23 +708,23 @@ footer {
|
||||
margin-right: 3px;
|
||||
}
|
||||
#updatebar {
|
||||
border: 1px solid #cccccc;
|
||||
background-image: -moz-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: -webkit-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
background-image: -o-linear-gradient(#ffffff, #eeeeee) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#eeeeee) !important;
|
||||
border: 1px solid #CCCCCC;
|
||||
background-image: -moz-linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
background-image: linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
background-image: -webkit-linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
background-image: -o-linear-gradient(#FFFFFF, #EEEEEE) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFFFFF, endColorstr=#EEEEEE) !important;
|
||||
-moz-border-radius: 7px;
|
||||
-webkit-border-radius: 7px;
|
||||
border-radius: 7px;
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
font-size: larger;
|
||||
right: 10px;
|
||||
-moz-box-shadow: 0px 0px 2px #aaaaaa;
|
||||
-webkit-box-shadow: 0px 0px 2px #aaaaaa;
|
||||
-o-box-shadow: 0px 0px 2px #aaaaaa;
|
||||
box-shadow: 0px 0px 2px #aaaaaa;
|
||||
-moz-box-shadow: 0px 0px 2px #aaa;
|
||||
-webkit-box-shadow: 0px 0px 2px #aaa;
|
||||
-o-box-shadow: 0px 0px 2px #aaa;
|
||||
box-shadow: 0px 0px 2px #aaa;
|
||||
padding: 7px 10px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
@@ -647,12 +738,12 @@ footer {
|
||||
-o-opacity: 0.8 !important;
|
||||
opacity: 0.8 !important;
|
||||
display: block;
|
||||
background-image: -moz-linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
background-image: linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
background-image: -webkit-linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
background-image: -o-linear-gradient(#fcf5c2, #fff6a9) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fcf5c2, endColorstr=#fff6a9) !important;
|
||||
background-image: -moz-linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
background-image: linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
background-image: -webkit-linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
background-image: -o-linear-gradient(#FCF5C2, #FFF6A9) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FCF5C2, endColorstr=#FFF6A9) !important;
|
||||
}
|
||||
#updatebar .msg {
|
||||
font-family: "Trebuchet MS", Helvetica, Arial, sans-serif;
|
||||
@@ -664,22 +755,22 @@ footer {
|
||||
top: 2px;
|
||||
}
|
||||
#updatebar.success {
|
||||
background-image: -moz-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: -webkit-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
background-image: -o-linear-gradient(#d3ffd7, #c2edc6) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#d3ffd7, endColorstr=#c2edc6) !important;
|
||||
background-image: -moz-linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
background-image: linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
background-image: -webkit-linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
background-image: -o-linear-gradient(#D3FFD7, #C2EDC6) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#D3FFD7, endColorstr=#C2EDC6) !important;
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
#updatebar.error {
|
||||
background-image: -moz-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: -webkit-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
background-image: -o-linear-gradient(#ffd3d3, #edc4c4) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffd3d3, endColorstr=#edc4c4) !important;
|
||||
background-image: -moz-linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
background-image: linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
background-image: -webkit-linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
background-image: -o-linear-gradient(#FFD3D3, #EDC4C4) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFD3D3, endColorstr=#EDC4C4) !important;
|
||||
padding: 15px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -708,23 +799,23 @@ footer {
|
||||
z-index: 99;
|
||||
}
|
||||
#subhead #subhead_container #subhead_menu a {
|
||||
background-image: -moz-linear-gradient(#f4f4f4, #e7e7e7) !important;
|
||||
background-image: linear-gradient(#f4f4f4, #e7e7e7) !important;
|
||||
background-image: -webkit-linear-gradient(#f4f4f4, #e7e7e7) !important;
|
||||
background-image: -o-linear-gradient(#f4f4f4, #e7e7e7) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f4f4f4, endColorstr=#e7e7e7) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#f4f4f4, endColorstr=#e7e7e7) !important;
|
||||
background-image: -moz-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: -webkit-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
background-image: -o-linear-gradient(#FAFAFA, #E7E7E7) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#FAFAFA, endColorstr=#E7E7E7) !important;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
#subhead #subhead_container #subhead_menu a:hover {
|
||||
background-image: -moz-linear-gradient(#599bdc, #3072b3) !important;
|
||||
background-image: linear-gradient(#599bdc, #3072b3) !important;
|
||||
background-image: -webkit-linear-gradient(#599bdc, #3072b3) !important;
|
||||
background-image: -o-linear-gradient(#599bdc, #3072b3) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599bdc, endColorstr=#3072b3) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599bdc, endColorstr=#3072b3) !important;
|
||||
background-image: -moz-linear-gradient(#599BDC, #3072B3) !important;
|
||||
background-image: linear-gradient(#599BDC, #3072B3) !important;
|
||||
background-image: -webkit-linear-gradient(#599BDC, #3072B3) !important;
|
||||
background-image: -o-linear-gradient(#599BDC, #3072B3) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599BDC, endColorstr=#3072B3) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#599BDC, endColorstr=#3072B3) !important;
|
||||
color: #FFF;
|
||||
border-color: #518CC6 #518CC6 #2A65A0;
|
||||
}
|
||||
@@ -747,6 +838,9 @@ div#searchbar {
|
||||
div#searchbar input[type=text] {
|
||||
border: 1px solid #DDD;
|
||||
border-top: 1px solid #CDCDCD;
|
||||
-moz-border-radius: 5px;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
-moz-box-shadow: 0 1px 0 #f1f1f1;
|
||||
-webkit-box-shadow: 0 1px 0 #f1f1f1;
|
||||
-o-box-shadow: 0 1px 0 #f1f1f1;
|
||||
@@ -755,9 +849,9 @@ div#searchbar input[type=text] {
|
||||
-webkit-box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
-o-box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
box-shadow: inset 0 1px 1px #e0e0e0;
|
||||
color: #999;
|
||||
color: #999999;
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
font-size: larger;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
margin-right: 5px;
|
||||
@@ -771,20 +865,6 @@ div#searchbar .mini-icon {
|
||||
margin-left: 9px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
.configtable legend {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 1px 1px 0 #FFFFFF;
|
||||
}
|
||||
.configtable tr td:last-child {
|
||||
border-left: 1px dotted #ddd;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.configtable td {
|
||||
padding-right: 15px;
|
||||
width: 50%;
|
||||
}
|
||||
.table_wrapper {
|
||||
_height: 302px;
|
||||
background-color: #FFF;
|
||||
@@ -828,10 +908,10 @@ div#artistheader {
|
||||
div#artistheader #artistImg {
|
||||
background: #ffffff url("../images/loader_black.gif") center no-repeat;
|
||||
border: 5px solid #FFF;
|
||||
-moz-box-shadow: 1px 1px 2px 0 #555555;
|
||||
-webkit-box-shadow: 1px 1px 2px 0 #555555;
|
||||
-o-box-shadow: 1px 1px 2px 0 #555555;
|
||||
box-shadow: 1px 1px 2px 0 #555555;
|
||||
-moz-box-shadow: 1px 1px 2px 0 #555;
|
||||
-webkit-box-shadow: 1px 1px 2px 0 #555;
|
||||
-o-box-shadow: 1px 1px 2px 0 #555;
|
||||
box-shadow: 1px 1px 2px 0 #555;
|
||||
float: left;
|
||||
height: 200px;
|
||||
margin-bottom: 30px;
|
||||
@@ -895,7 +975,7 @@ div#artistheader h2 a {
|
||||
vertical-align: middle;
|
||||
}
|
||||
#artist_table #artistImg {
|
||||
background: url("../images/loader_black.gif") no-repeat scroll center center #ffffff;
|
||||
background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
|
||||
border: 3px solid #FFFFFF;
|
||||
box-shadow: 1px 1px 2px 0 #555555;
|
||||
float: left;
|
||||
@@ -947,10 +1027,10 @@ div#artistheader h2 a {
|
||||
#albumheader #albumImg {
|
||||
background: #ffffff url("../images/loader_black.gif") center no-repeat;
|
||||
border: 5px solid #FFF;
|
||||
-moz-box-shadow: 1px 1px 2px 0 #555555;
|
||||
-webkit-box-shadow: 1px 1px 2px 0 #555555;
|
||||
-o-box-shadow: 1px 1px 2px 0 #555555;
|
||||
box-shadow: 1px 1px 2px 0 #555555;
|
||||
-moz-box-shadow: 1px 1px 2px 0 #555;
|
||||
-webkit-box-shadow: 1px 1px 2px 0 #555;
|
||||
-o-box-shadow: 1px 1px 2px 0 #555;
|
||||
box-shadow: 1px 1px 2px 0 #555;
|
||||
float: left;
|
||||
height: 200px;
|
||||
margin-bottom: 30px;
|
||||
@@ -993,6 +1073,13 @@ div#artistheader h2 a {
|
||||
#albumheader .albuminfo li:last-child {
|
||||
border: none;
|
||||
}
|
||||
.nopad {
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
#album_table {
|
||||
background-color: #FFF;
|
||||
}
|
||||
@@ -1009,7 +1096,6 @@ div#artistheader h2 a {
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
#album_table th#status,
|
||||
#album_table th#albumart {
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
@@ -1024,11 +1110,47 @@ div#artistheader h2 a {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
#album_table th#albumname {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
#album_table th#bitrate,
|
||||
#album_table th#albumformat {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
#album_table th#type {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
#album_table td {
|
||||
vertical-align: middle;
|
||||
font-size: larger;
|
||||
}
|
||||
#album_table td#albumname,
|
||||
#album_table td#reldate,
|
||||
#album_table td#type,
|
||||
#album_table td#bitrate,
|
||||
#album_table td#albumformat,
|
||||
#album_table td#score,
|
||||
#album_table td#wantlossless {
|
||||
text-align: center;
|
||||
}
|
||||
#album_table td#select {
|
||||
text-align: left;
|
||||
}
|
||||
#album_table td#albumart {
|
||||
text-align: left;
|
||||
}
|
||||
#album_table td#albumart img {
|
||||
background: #FFF;
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px;
|
||||
}
|
||||
#album_table td#status {
|
||||
text-align: center;
|
||||
}
|
||||
#album_table td#status a#wantlossless {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1071,20 +1193,19 @@ div#artistheader h2 a {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#downloads_table th#title {
|
||||
#downloads_table th {
|
||||
text-align: center;
|
||||
}
|
||||
#downloads_table th#title {
|
||||
min-width: 500px;
|
||||
}
|
||||
#downloads_table th#size {
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
#downloads_table th#provider {
|
||||
text-align: center;
|
||||
min-width: 100px;
|
||||
}
|
||||
#downloads_table th#kind {
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
#history_table {
|
||||
@@ -1129,87 +1250,54 @@ div#artistheader h2 a {
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
#searchresults_table th {
|
||||
font-size: larger;
|
||||
text-align: left;
|
||||
min-width: 50px;
|
||||
}
|
||||
#searchresults_table th#artistnamesmall {
|
||||
min-width: 125px;
|
||||
}
|
||||
#searchresults_table th#reldate {
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
#searchresults_table th#albumname {
|
||||
min-width: 250px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table th#artistname {
|
||||
min-width: 325px;
|
||||
}
|
||||
#searchresults_table th#score {
|
||||
min-width: 75px;
|
||||
text-align: center;
|
||||
}
|
||||
#searchresults_table td {
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
font-size: larger;
|
||||
min-width: 50px;
|
||||
}
|
||||
#searchresults_table th#artistnamesmall {
|
||||
min-width: 125px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
#searchresults_table td#artistnamesmall {
|
||||
min-width: 100px;
|
||||
}
|
||||
#searchresults_table th#reldate {
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table th#format {
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table th#tracks {
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table #artistImg {
|
||||
background: url("../images/loader_black.gif") no-repeat scroll center center #ffffff;
|
||||
border: 3px solid #FFFFFF;
|
||||
box-shadow: 1px 1px 2px 0 #555555;
|
||||
float: left;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
text-indent: -3000px;
|
||||
width: 50px;
|
||||
#searchresults_table td#reldate {
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
#searchresults_table td#albumname {
|
||||
min-width: 250px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
min-width: 220px;
|
||||
}
|
||||
#searchresults_table td#artistname {
|
||||
min-width: 300px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#searchresults_table td#artistnamesmall {
|
||||
min-width: 100px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table td#reldate {
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table td#format {
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table td#tracks {
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
#searchresults_table td#add {
|
||||
vertical-align: middle;
|
||||
#searchresults_table td#score {
|
||||
min-width: 75px;
|
||||
}
|
||||
#searchresults_table td#score .bar {
|
||||
width: 100px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #cccccc;
|
||||
border: 1px solid #CCCCCC;
|
||||
padding: 1px;
|
||||
background-color: #FFF;
|
||||
}
|
||||
@@ -1220,12 +1308,22 @@ div#artistheader h2 a {
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
background-image: -moz-linear-gradient(#a3e532, #90cc2a) !important;
|
||||
background-image: linear-gradient(#a3e532, #90cc2a) !important;
|
||||
background-image: -webkit-linear-gradient(#a3e532, #90cc2a) !important;
|
||||
background-image: -o-linear-gradient(#a3e532, #90cc2a) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
|
||||
background-image: -moz-linear-gradient(#A3E532, #90CC2A) !important;
|
||||
background-image: linear-gradient(#A3E532, #90CC2A) !important;
|
||||
background-image: -webkit-linear-gradient(#A3E532, #90CC2A) !important;
|
||||
background-image: -o-linear-gradient(#A3E532, #90CC2A) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
|
||||
}
|
||||
#searchresults_table #artistImg {
|
||||
background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
|
||||
border: 3px solid #FFFFFF;
|
||||
box-shadow: 1px 1px 2px 0 #555555;
|
||||
float: left;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
text-indent: -3000px;
|
||||
width: 50px;
|
||||
}
|
||||
.progress-container {
|
||||
background: #FFF;
|
||||
@@ -1237,12 +1335,12 @@ div#artistheader h2 a {
|
||||
width: 100px;
|
||||
}
|
||||
.progress-container > div {
|
||||
background-image: -moz-linear-gradient(#a3e532, #90cc2a) !important;
|
||||
background-image: linear-gradient(#a3e532, #90cc2a) !important;
|
||||
background-image: -webkit-linear-gradient(#a3e532, #90cc2a) !important;
|
||||
background-image: -o-linear-gradient(#a3e532, #90cc2a) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#a3e532, endColorstr=#90cc2a) !important;
|
||||
background-image: -moz-linear-gradient(#A3E532, #90CC2A) !important;
|
||||
background-image: linear-gradient(#A3E532, #90CC2A) !important;
|
||||
background-image: -webkit-linear-gradient(#A3E532, #90CC2A) !important;
|
||||
background-image: -o-linear-gradient(#A3E532, #90CC2A) !important;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#A3E532, endColorstr=#90CC2A) !important;
|
||||
height: 14px;
|
||||
}
|
||||
.havetracks {
|
||||
@@ -1270,14 +1368,14 @@ div#artistheader h2 a {
|
||||
text-align: left;
|
||||
margin-left: 10px;
|
||||
margin-top: -5px;
|
||||
color: #cccccc;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
#actions {
|
||||
float: right;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
margin-top: -5px;
|
||||
color: #cccccc;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
#actions .ui-icon {
|
||||
position: relative;
|
||||
@@ -1301,7 +1399,9 @@ div#artistheader h2 a {
|
||||
position: relative;
|
||||
top: 7px;
|
||||
}
|
||||
#trashcan {margin-top:15px;}
|
||||
#trashcan {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.cloudtag {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -1398,69 +1498,6 @@ div#artistheader h2 a {
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
content: "\0020";
|
||||
display: block;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.clearfix {
|
||||
zoom: 1px;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.override-float {
|
||||
float: none !important;
|
||||
margin-bottom: 0px !important;
|
||||
clear: none !important;
|
||||
display: inline !important;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
#album_table th#albumname,
|
||||
#upcoming_table th#artistname,
|
||||
#wanted_table th#artistname {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
#album_table th#type,
|
||||
#track_table th#duration {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
#album_table th#bitrate,
|
||||
#album_table th#albumformat {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
#album_table td#select,
|
||||
#album_table td#albumart {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#album_table td#albumname,
|
||||
#album_table td#reldate,
|
||||
#album_table td#type,
|
||||
#track_table td#duration,
|
||||
#upcoming_table td#select,
|
||||
#upcoming_table td#status,
|
||||
#wanted_table td#select,
|
||||
#wanted_table td#status {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#album_table td#status,
|
||||
#album_table td#bitrate,
|
||||
#album_table td#albumformat,
|
||||
#album_table td#wantlossless {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
div#albumheader .albuminfo li span,
|
||||
div#artistheader h3 span {
|
||||
font-weight: 400;
|
||||
@@ -1468,16 +1505,10 @@ div#artistheader h3 span {
|
||||
#track_table th#bitrate,
|
||||
#track_table th#format,
|
||||
#upcoming_table th#type,
|
||||
#wanted_table th#type,
|
||||
#searchresults_table th#score {
|
||||
#wanted_table th#type {
|
||||
min-width: 75px;
|
||||
text-align: center;
|
||||
}
|
||||
#searchresults_table th#scoresmall {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
#track_table td#bitrate,
|
||||
#track_table td#format {
|
||||
font-size: 12px;
|
||||
@@ -1490,6 +1521,24 @@ div#artistheader h3 span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#upcoming_table th#artistname,
|
||||
#wanted_table th#artistname {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
#track_table th#duration {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
#track_table td#duration,
|
||||
#upcoming_table td#select,
|
||||
#upcoming_table td#status,
|
||||
#wanted_table td#select,
|
||||
#wanted_table td#status {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#upcoming_table td#albumart img,
|
||||
#wanted_table td#albumart img {
|
||||
background: #FFF;
|
||||
@@ -1536,14 +1585,13 @@ div#artistheader h3 span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
#upcoming_table td#type,
|
||||
#wanted_table td#type,
|
||||
#searchresults_table td#score {
|
||||
#wanted_table td#type {
|
||||
min-width: 75px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
table tr td#status a {
|
||||
color: #4183c4;
|
||||
color: #4183C4;
|
||||
}
|
||||
.ie7 input[type="checkbox"] {
|
||||
vertical-align: baseline;
|
||||
|
||||
@@ -26,6 +26,19 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Clearfix from http://stackoverflow.com/a/8554054/1115187
|
||||
.clearfix()
|
||||
{
|
||||
&:after
|
||||
{
|
||||
content: " "; /* Older browser do not support empty content */
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// Links
|
||||
a {
|
||||
color: @link-color;
|
||||
@@ -76,18 +89,149 @@ hr {
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
small {
|
||||
font-size: 85%;
|
||||
|
||||
small, .small
|
||||
{
|
||||
font-size: xx-small;
|
||||
}
|
||||
|
||||
// -------------------------------------------------
|
||||
// Config Page
|
||||
// -------------------------------------------------
|
||||
|
||||
// OLD VERSION:
|
||||
.configtable {
|
||||
legend {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 1px 1px 0 #FFFFFF;
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
&:last-child {
|
||||
border-left: 1px dotted #ddd;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
padding-right: 15px;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// NEW VERSION:
|
||||
.config
|
||||
{
|
||||
legend {
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
text-shadow: 1px 1px 0 #FFFFFF;
|
||||
}
|
||||
|
||||
.caption
|
||||
{
|
||||
color: @caption-color;
|
||||
line-height: @base-font-size;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.tab
|
||||
{
|
||||
.columnRule( 1px solid @border-color );
|
||||
.columnCount(2);
|
||||
.columnGap(4em);
|
||||
|
||||
.message
|
||||
{
|
||||
.columnSpan(all);
|
||||
display:block;
|
||||
background-color:transparent;
|
||||
margin-bottom:1em;
|
||||
}
|
||||
}
|
||||
|
||||
.block
|
||||
{
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid-column;
|
||||
}
|
||||
|
||||
.option
|
||||
{
|
||||
// TODO : need responsive - remove floating, use flexbox
|
||||
.clearfix();
|
||||
margin-bottom: 1em;
|
||||
font-size: @base-font-size;
|
||||
|
||||
.input-wrapper
|
||||
{
|
||||
// TODO : need responsive
|
||||
float: left;
|
||||
}
|
||||
|
||||
label
|
||||
{
|
||||
// TODO : need responsive
|
||||
width: 170px;
|
||||
font-size: @base-font-size;
|
||||
// TODO : need responsive
|
||||
float: left;
|
||||
}
|
||||
|
||||
input
|
||||
{
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=number],
|
||||
{
|
||||
border: 1px solid #DDD;
|
||||
border-top: 1px solid #CDCDCD;
|
||||
.shadow(inset 0 1px 1px #e0e0e0);
|
||||
color: @text-color;
|
||||
font-size: larger;
|
||||
// TODO : need responsive
|
||||
max-width: 230px;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
input[type=number]
|
||||
{
|
||||
max-width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-option-block
|
||||
{
|
||||
margin-left:1em;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
&.albumArt {
|
||||
.-less-albumArt(@size)
|
||||
{
|
||||
float: left;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
min-height: @size;
|
||||
min-width: @size;
|
||||
max-width: 250px;
|
||||
max-height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.albumArt
|
||||
{
|
||||
.-less-albumArt(100%);
|
||||
}
|
||||
&.albumArt-nostretch
|
||||
{
|
||||
.-less-albumArt(64px);
|
||||
}
|
||||
}
|
||||
.title {
|
||||
margin-bottom:20px;
|
||||
@@ -105,9 +249,9 @@ table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
th {
|
||||
.gradient(#FAFAFA, #EAEAEA);
|
||||
.gradient(@gradient-color-1, @gradient-color-2);
|
||||
border-left: 1px solid #E0E0E0;
|
||||
.shadow(1px 0 0 #FAFAFA);
|
||||
.shadow(1px 0 0 @gradient-color-1);
|
||||
text-shadow:1px 1px 0 #FFFFFF ;
|
||||
input[type="checkbox"]{ vertical-align: middle;}
|
||||
&:first-child { border-left:0;.shadow(none)}
|
||||
@@ -124,7 +268,23 @@ table {
|
||||
|
||||
|
||||
// Forms
|
||||
select, input, textarea, button { font: 99%;}
|
||||
select, input, textarea, button
|
||||
{
|
||||
font-size: 99%;
|
||||
}
|
||||
|
||||
select
|
||||
{
|
||||
.rounded(@heading-input-radius);
|
||||
background: @brand-color;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||
color: #fff;
|
||||
padding: 3px 10px;
|
||||
text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
|
||||
textarea {overflow: auto;}
|
||||
input { .rounded(3px);}
|
||||
input:invalid, textarea:invalid {
|
||||
@@ -139,7 +299,7 @@ button {
|
||||
overflow: visible;
|
||||
width: auto;
|
||||
}
|
||||
input,select,form .checkbox input,.configtable td#middle, #artist_table td#have,#album_table td#have { vertical-align: middle; }
|
||||
input,select,form .checkbox input,.configtable td#middle, #artist_table td#have { vertical-align: middle; }
|
||||
|
||||
input[type="radio"]{ vertical-align: text-bottom; }
|
||||
::-moz-selection, ::selection {
|
||||
@@ -148,7 +308,7 @@ input[type="radio"]{ vertical-align: text-bottom; }
|
||||
text-shadow: none;
|
||||
}
|
||||
input[type=submit], input[type=button] {
|
||||
.rounded(5px);
|
||||
.rounded(@heading-input-radius);
|
||||
background: #222222 url("../images/button.png") repeat-x;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
|
||||
@@ -181,8 +341,8 @@ form {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
font-family: Helvetica, Arial;
|
||||
margin-bottom: 10px;
|
||||
label {
|
||||
display: block;
|
||||
@@ -190,7 +350,7 @@ form {
|
||||
font-size: 12px;
|
||||
line-height: normal;
|
||||
padding-top: 7px;
|
||||
width: 175px;
|
||||
width: 170px;
|
||||
|
||||
&.inline {
|
||||
margin-right: 5px;
|
||||
@@ -216,7 +376,7 @@ form {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
line-height: 12px;
|
||||
margin-left: 175px;
|
||||
margin-left: 170px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
@@ -276,7 +436,7 @@ ul#nav {
|
||||
float: right;
|
||||
margin: 0;
|
||||
padding: 0 0 0 10px;
|
||||
border-left: 1px solid #FAFAFA;
|
||||
border-left: 1px solid @gradient-color-1;
|
||||
.shadow(-1px 0 0 #e0e0e0);
|
||||
height: 58px;
|
||||
li {
|
||||
@@ -298,8 +458,8 @@ ul#nav {
|
||||
.gradient(#F1F1F1, #E0E0E0);
|
||||
border: 1px solid #DDDDDD;
|
||||
.rounded(3px);
|
||||
.shadow(0 1px 0 #FAFAFA);
|
||||
.shadow(0 1px 0 #FAFAFA inset);
|
||||
.shadow(0 1px 0 @gradient-color-1);
|
||||
.shadow(0 1px 0 @gradient-color-1 inset);
|
||||
-webkit-transition:color .2s ease-in;
|
||||
-moz-transition:color .2s ease-in;
|
||||
-o-transition:color .2s ease-in;
|
||||
@@ -323,8 +483,8 @@ ul#nav {
|
||||
}
|
||||
// Layout
|
||||
header {
|
||||
.gradient(#fafafa, #eaeaea);
|
||||
border-bottom: 1px solid #CACACA;
|
||||
.gradient(@gradient-color-1, @gradient-color-2);
|
||||
border-bottom: 1px solid @border-color;
|
||||
.shadow(0 0 10px rgba(0, 0, 0, 0.1));
|
||||
height: 58px;
|
||||
position: fixed;
|
||||
@@ -334,7 +494,7 @@ header {
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 960px;
|
||||
width: 990px;
|
||||
}
|
||||
#logo {
|
||||
float: left;
|
||||
@@ -372,12 +532,12 @@ footer {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
#ajaxMsg {
|
||||
.ajaxMsg {
|
||||
border: 1px solid @border-color;
|
||||
.gradient(#FFFFFF, #EEEEEE);
|
||||
.rounded(7px);
|
||||
display: none;
|
||||
font-size: 14px;
|
||||
font-size: larger;
|
||||
right: 10px;
|
||||
.shadow(0px 0px 2px #aaa);
|
||||
padding: 7px 10px;
|
||||
@@ -417,9 +577,9 @@ footer {
|
||||
}
|
||||
|
||||
#updatebar {
|
||||
#ajaxMsg;
|
||||
.ajaxMsg;
|
||||
display: block;
|
||||
.gradient(#FCF5C2,@msg-bg);
|
||||
.gradient(#FCF5C2, @msg-bg);
|
||||
}
|
||||
|
||||
// Subheader
|
||||
@@ -434,29 +594,39 @@ footer {
|
||||
list-style-type: none;
|
||||
width: 100%;
|
||||
z-index: 998;
|
||||
#subhead_menu {
|
||||
#subhead_menu
|
||||
{
|
||||
float: right;
|
||||
margin-top: 5px;
|
||||
margin-top: 0px;
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
a {
|
||||
.gradient(#F4F4F4, #e7e7e7);
|
||||
a
|
||||
{
|
||||
.gradient(@gradient-color-1, @gradient-color-2);
|
||||
font-family: @base-font-face;
|
||||
font-size: 12px;
|
||||
font-size: @base-font-size;
|
||||
font-weight: normal;
|
||||
&:hover {
|
||||
.gradient(#599BDC, #3072B3);
|
||||
color: #FFF;
|
||||
border-color: #518CC6 #518CC6 #2A65A0;
|
||||
&:hover
|
||||
{
|
||||
.gradient(#599BDC, #3072B3);
|
||||
color: #FFF;
|
||||
border-color: #518CC6 #518CC6 #2A65A0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#back_to_previous_link
|
||||
{
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
div#searchbar {
|
||||
border-left: 1px solid #FAFAFA;
|
||||
border-left: 1px solid @gradient-color-1;
|
||||
.shadow(-1px 0 0 #e0e0e0);
|
||||
padding: 15px 0 15px 20px;
|
||||
position: absolute;
|
||||
@@ -465,16 +635,17 @@ div#searchbar {
|
||||
input[type=text] {
|
||||
border: 1px solid #DDD;
|
||||
border-top: 1px solid #CDCDCD;
|
||||
.rounded(@heading-input-radius);
|
||||
.shadow(0 1px 0 #f1f1f1);
|
||||
.shadow(inset 0 1px 1px #e0e0e0);
|
||||
color: #999;
|
||||
color: @caption-color;
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
font-size: larger;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
margin-right: 5px;
|
||||
padding: 4px 5px 4px 25px;
|
||||
width: 150px;
|
||||
width: 185px;
|
||||
}
|
||||
.mini-icon {
|
||||
color: #999;
|
||||
@@ -485,28 +656,6 @@ div#searchbar {
|
||||
}
|
||||
}
|
||||
|
||||
// Config Page
|
||||
.configtable {
|
||||
legend {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 1px 1px 0 #FFFFFF;
|
||||
}
|
||||
tr {
|
||||
td {
|
||||
&:last-child {
|
||||
border-left: 1px dotted #ddd;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
padding-right: 15px;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// TABLES
|
||||
|
||||
// wrappers
|
||||
@@ -514,7 +663,7 @@ div#searchbar {
|
||||
_height: 302px;
|
||||
background-color: #FFF;
|
||||
clear: both;
|
||||
margin: 30px auto 0;
|
||||
margin: 60px auto 0;
|
||||
min-height: 100px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -728,6 +877,15 @@ div#artistheader {
|
||||
}
|
||||
}
|
||||
|
||||
.nopad
|
||||
{
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#album_table {
|
||||
background-color: #FFF;
|
||||
th#select {
|
||||
@@ -741,7 +899,8 @@ div#artistheader {
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
th#status, th#albumart {
|
||||
th#albumart
|
||||
{
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -755,16 +914,65 @@ div#artistheader {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
td#albumart {
|
||||
img {
|
||||
background: #FFF;
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px;
|
||||
}
|
||||
th#albumname
|
||||
{
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
td#status {
|
||||
a#wantlossless {
|
||||
white-space: nowrap;
|
||||
th#bitrate,
|
||||
th#albumformat
|
||||
{
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
th#type
|
||||
{
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
td
|
||||
{
|
||||
vertical-align: middle;
|
||||
font-size: larger;
|
||||
|
||||
&#have
|
||||
{
|
||||
}
|
||||
|
||||
&#albumname,
|
||||
&#reldate,
|
||||
&#type,
|
||||
&#bitrate,
|
||||
&#albumformat,
|
||||
&#score,
|
||||
&#wantlossless
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&#select
|
||||
{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&#albumart
|
||||
{
|
||||
text-align: left;
|
||||
img {
|
||||
background: #FFF;
|
||||
border: 1px solid #ccc;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
&#status
|
||||
{
|
||||
text-align: center;
|
||||
a#wantlossless
|
||||
{
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -813,6 +1021,31 @@ div#artistheader {
|
||||
}
|
||||
}
|
||||
|
||||
#downloads_table
|
||||
{
|
||||
th
|
||||
{
|
||||
text-align: center;
|
||||
|
||||
&#title
|
||||
{
|
||||
min-width: 500px;
|
||||
}
|
||||
&#size
|
||||
{
|
||||
min-width: 80px;
|
||||
}
|
||||
&#provider
|
||||
{
|
||||
min-width: 100px;
|
||||
}
|
||||
&#kind
|
||||
{
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// History
|
||||
#history_table {
|
||||
background-color: #FFF;
|
||||
@@ -858,18 +1091,111 @@ div#artistheader {
|
||||
padding: 2px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Searchresults
|
||||
#searchresults_table {
|
||||
th#albumname {
|
||||
min-width: 225px;
|
||||
#searchresults_table
|
||||
{
|
||||
th
|
||||
{
|
||||
font-size: larger;
|
||||
text-align: left;
|
||||
min-width: 50px;
|
||||
|
||||
&#artistnamesmall
|
||||
{
|
||||
min-width: 125px;
|
||||
}
|
||||
&#reldate
|
||||
{
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
&#format
|
||||
{
|
||||
}
|
||||
&#tracks
|
||||
{
|
||||
}
|
||||
&#albumname
|
||||
{
|
||||
min-width: 250px;
|
||||
}
|
||||
&#artistname
|
||||
{
|
||||
min-width: 325px;
|
||||
}
|
||||
&#score
|
||||
{
|
||||
min-width: 75px;
|
||||
text-align: center;
|
||||
}
|
||||
&#scoresmall
|
||||
{
|
||||
}
|
||||
}
|
||||
th#artistname {
|
||||
min-width: 325px;
|
||||
|
||||
td
|
||||
{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
font-size: larger;
|
||||
min-width: 50px;
|
||||
|
||||
&#artistnamesmall
|
||||
{
|
||||
min-width: 100px;
|
||||
}
|
||||
&#reldate
|
||||
{
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
&#format
|
||||
{
|
||||
}
|
||||
&#tracks
|
||||
{
|
||||
}
|
||||
|
||||
&#albumname
|
||||
{
|
||||
min-width: 220px;
|
||||
}
|
||||
&#artistname
|
||||
{
|
||||
min-width: 300px;
|
||||
}
|
||||
&#add
|
||||
{
|
||||
}
|
||||
|
||||
&#score
|
||||
{
|
||||
min-width: 75px;
|
||||
|
||||
.bar
|
||||
{
|
||||
width: 100px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid @border-color;
|
||||
padding: 1px;
|
||||
background-color: #FFF;
|
||||
.score
|
||||
{
|
||||
height: 14px;
|
||||
color: @text-color;
|
||||
color: #FFF;
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
.gradient(#A3E532,#90CC2A);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#artistImg {
|
||||
background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
|
||||
background: url("../images/loader_black.gif") no-repeat scroll center center #FFFFFF;
|
||||
border: 3px solid #FFFFFF;
|
||||
box-shadow: 1px 1px 2px 0 #555555;
|
||||
float: left;
|
||||
@@ -878,36 +1204,8 @@ div#artistheader {
|
||||
text-indent: -3000px;
|
||||
width: 50px;
|
||||
}
|
||||
td#albumname {
|
||||
min-width: 200px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td#artistname {
|
||||
min-width: 300px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
td#add {vertical-align: middle;}
|
||||
td#score {
|
||||
.bar {
|
||||
width: 100px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid @border-color;
|
||||
padding: 1px;
|
||||
background-color: #FFF;
|
||||
.score {
|
||||
height: 14px;
|
||||
color: @text-color;
|
||||
color: #FFF;
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
.gradient(#A3E532,#90CC2A);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
background: #FFF;
|
||||
border: 1px solid #ccc;
|
||||
@@ -981,6 +1279,12 @@ div#artistheader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#trashcan
|
||||
{
|
||||
margin-top:15px;
|
||||
}
|
||||
|
||||
.cloudtag {
|
||||
font-size: 16px;
|
||||
#cloud {
|
||||
@@ -1073,48 +1377,11 @@ div#artistheader {
|
||||
}
|
||||
.invisible { visibility: hidden; }
|
||||
|
||||
.clearfix:before, .clearfix:after {
|
||||
content: "\0020";
|
||||
display: block;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.clearfix {
|
||||
zoom: 1px;
|
||||
&:after { clear: both; }
|
||||
}
|
||||
|
||||
// Table width
|
||||
#album_table th#albumname, #upcoming_table th#artistname, #wanted_table th#artistname {
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
#album_table th#type, #track_table th#duration {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
#album_table th#bitrate, #album_table th#albumformat {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
#album_table td#select, #album_table td#albumart {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#album_table td#albumname,#album_table td#reldate, #album_table td#type, #track_table td#duration, #upcoming_table td#select, #upcoming_table td#status, #wanted_table td#select, #wanted_table td#status {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#album_table td#status, #album_table td#bitrate, #album_table td#albumformat, #album_table td#wantlossless {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
div#albumheader .albuminfo li span, div#artistheader h3 span {
|
||||
font-weight: 400;
|
||||
}
|
||||
#track_table th#bitrate, #track_table th#format, #upcoming_table th#type, #wanted_table th#type, #searchresults_table th#score {
|
||||
#track_table th#bitrate, #track_table th#format, #upcoming_table th#type, #wanted_table th#type
|
||||
{
|
||||
min-width: 75px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1128,6 +1395,22 @@ div#albumheader .albuminfo li span, div#artistheader h3 span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#upcoming_table th#artistname, #wanted_table th#artistname
|
||||
{
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
#track_table th#duration {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
width: 175px;
|
||||
}
|
||||
#track_table td#duration, #upcoming_table td#select, #upcoming_table td#status, #wanted_table td#select, #wanted_table td#status {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#upcoming_table, #wanted_table {
|
||||
td#albumart {
|
||||
img {
|
||||
@@ -1169,7 +1452,8 @@ div#albumheader .albuminfo li span, div#artistheader h3 span {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#upcoming_table td#type, #wanted_table td#type, #searchresults_table td#score {
|
||||
#upcoming_table td#type, #wanted_table td#type
|
||||
{
|
||||
min-width: 75px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
|
||||
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
+311
-40
@@ -14,10 +14,11 @@
|
||||
<glyph unicode="®" horiz-adv-x="1792" />
|
||||
<glyph unicode="´" horiz-adv-x="1792" />
|
||||
<glyph unicode="Æ" horiz-adv-x="1792" />
|
||||
<glyph unicode="Ø" horiz-adv-x="1792" />
|
||||
<glyph unicode=" " horiz-adv-x="768" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode=" " horiz-adv-x="1537" />
|
||||
<glyph unicode=" " horiz-adv-x="768" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode=" " horiz-adv-x="1537" />
|
||||
<glyph unicode=" " horiz-adv-x="512" />
|
||||
<glyph unicode=" " horiz-adv-x="384" />
|
||||
<glyph unicode=" " horiz-adv-x="256" />
|
||||
@@ -30,7 +31,7 @@
|
||||
<glyph unicode="™" horiz-adv-x="1792" />
|
||||
<glyph unicode="∞" horiz-adv-x="1792" />
|
||||
<glyph unicode="≠" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="500" d="M0 0z" />
|
||||
<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1699 1350q0 -35 -43 -78l-632 -632v-768h320q26 0 45 -19t19 -45t-19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45t45 19h320v768l-632 632q-43 43 -43 78q0 23 18 36.5t38 17.5t43 4h1408q23 0 43 -4t38 -17.5t18 -36.5z" />
|
||||
<glyph unicode="" d="M1536 1312v-1120q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v537l-768 -237v-709q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89 t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v967q0 31 19 56.5t49 35.5l832 256q12 4 28 4q40 0 68 -28t28 -68z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -52 -38 -90t-90 -38q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5 t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" />
|
||||
@@ -52,7 +53,7 @@
|
||||
<glyph unicode="" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1536 749v-222q0 -12 -8 -23t-20 -13l-185 -28q-19 -54 -39 -91q35 -50 107 -138q10 -12 10 -25t-9 -23q-27 -37 -99 -108t-94 -71q-12 0 -26 9l-138 108q-44 -23 -91 -38 q-16 -136 -29 -186q-7 -28 -36 -28h-222q-14 0 -24.5 8.5t-11.5 21.5l-28 184q-49 16 -90 37l-141 -107q-10 -9 -25 -9q-14 0 -25 11q-126 114 -165 168q-7 10 -7 23q0 12 8 23q15 21 51 66.5t54 70.5q-27 50 -41 99l-183 27q-13 2 -21 12.5t-8 23.5v222q0 12 8 23t19 13 l186 28q14 46 39 92q-40 57 -107 138q-10 12 -10 24q0 10 9 23q26 36 98.5 107.5t94.5 71.5q13 0 26 -10l138 -107q44 23 91 38q16 136 29 186q7 28 36 28h222q14 0 24.5 -8.5t11.5 -21.5l28 -184q49 -16 90 -37l142 107q9 9 24 9q13 0 25 -10q129 -119 165 -170q7 -8 7 -22 q0 -12 -8 -23q-15 -21 -51 -66.5t-54 -70.5q26 -50 41 -98l183 -28q13 -2 21 -12.5t8 -23.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M512 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM768 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1024 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1152 76v948h-896v-948q0 -22 7 -40.5t14.5 -27t10.5 -8.5h832q3 0 10.5 8.5t14.5 27t7 40.5zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832 q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M128 0h1024v768h-416q-40 0 -68 28t-28 68v416h-512v-1280zM768 896h376q-10 29 -22 41l-313 313q-12 12 -41 22v-376zM1280 864v-896q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h640q40 0 88 -20t76 -48l312 -312q28 -28 48 -76t20 -88z " />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z " />
|
||||
<glyph unicode="" d="M896 992v-448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1111 540v4l-24 320q-1 13 -11 22.5t-23 9.5h-186q-13 0 -23 -9.5t-11 -22.5l-24 -320v-4q-1 -12 8 -20t21 -8h244q12 0 21 8t8 20zM1870 73q0 -73 -46 -73h-704q13 0 22 9.5t8 22.5l-20 256q-1 13 -11 22.5t-23 9.5h-272q-13 0 -23 -9.5t-11 -22.5l-20 -256 q-1 -13 8 -22.5t22 -9.5h-704q-46 0 -46 73q0 54 26 116l417 1044q8 19 26 33t38 14h339q-13 0 -23 -9.5t-11 -22.5l-15 -192q-1 -14 8 -23t22 -9h166q13 0 22 9t8 23l-15 192q-1 13 -11 22.5t-23 9.5h339q20 0 38 -14t26 -33l417 -1044q26 -62 26 -116z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1280 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 416v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h465l135 -136 q58 -56 136 -56t136 56l136 136h464q40 0 68 -28t28 -68zM1339 985q17 -41 -14 -70l-448 -448q-18 -19 -45 -19t-45 19l-448 448q-31 29 -14 70q17 39 59 39h256v448q0 26 19 45t45 19h256q26 0 45 -19t19 -45v-448h256q42 0 59 -39z" />
|
||||
@@ -77,11 +78,11 @@
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289q0 34 19.5 62t52.5 41q21 9 44 9h1048z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M384 0h896v256h-896v-256zM384 640h896v384h-160q-40 0 -68 28t-28 68v160h-640v-640zM1536 576q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 576v-416q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-160q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68 v160h-224q-13 0 -22.5 9.5t-9.5 22.5v416q0 79 56.5 135.5t135.5 56.5h64v544q0 40 28 68t68 28h672q40 0 88 -20t76 -48l152 -152q28 -28 48 -76t20 -88v-256h64q79 0 135.5 -56.5t56.5 -135.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M960 864q119 0 203.5 -84.5t84.5 -203.5t-84.5 -203.5t-203.5 -84.5t-203.5 84.5t-84.5 203.5t84.5 203.5t203.5 84.5zM1664 1280q106 0 181 -75t75 -181v-896q0 -106 -75 -181t-181 -75h-1408q-106 0 -181 75t-75 181v896q0 106 75 181t181 75h224l51 136 q19 49 69.5 84.5t103.5 35.5h512q53 0 103.5 -35.5t69.5 -84.5l51 -136h224zM960 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M725 977l-170 -450q73 -1 153.5 -2t119 -1.5t52.5 -0.5l29 2q-32 95 -92 241q-53 132 -92 211zM21 -128h-21l2 79q22 7 80 18q89 16 110 31q20 16 48 68l237 616l280 724h75h53l11 -21l205 -480q103 -242 124 -297q39 -102 96 -235q26 -58 65 -164q24 -67 65 -149 q22 -49 35 -57q22 -19 69 -23q47 -6 103 -27q6 -39 6 -57q0 -14 -1 -26q-80 0 -192 8q-93 8 -189 8q-79 0 -135 -2l-200 -11l-58 -2q0 45 4 78l131 28q56 13 68 23q12 12 12 27t-6 32l-47 114l-92 228l-450 2q-29 -65 -104 -274q-23 -64 -23 -84q0 -31 17 -43 q26 -21 103 -32q3 0 13.5 -2t30 -5t40.5 -6q1 -28 1 -58q0 -17 -2 -27q-66 0 -349 20l-48 -8q-81 -14 -167 -14z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M555 15q76 -32 140 -32q131 0 216 41t122 113q38 70 38 181q0 114 -41 180q-58 94 -141 126q-80 32 -247 32q-74 0 -101 -10v-144l-1 -173l3 -270q0 -15 12 -44zM541 761q43 -7 109 -7q175 0 264 65t89 224q0 112 -85 187q-84 75 -255 75q-52 0 -130 -13q0 -44 2 -77 q7 -122 6 -279l-1 -98q0 -43 1 -77zM0 -128l2 94q45 9 68 12q77 12 123 31q17 27 21 51q9 66 9 194l-2 497q-5 256 -9 404q-1 87 -11 109q-1 4 -12 12q-18 12 -69 15q-30 2 -114 13l-4 83l260 6l380 13l45 1q5 0 14 0.5t14 0.5q1 0 21.5 -0.5t40.5 -0.5h74q88 0 191 -27 q43 -13 96 -39q57 -29 102 -76q44 -47 65 -104t21 -122q0 -70 -32 -128t-95 -105q-26 -20 -150 -77q177 -41 267 -146q92 -106 92 -236q0 -76 -29 -161q-21 -62 -71 -117q-66 -72 -140 -108q-73 -36 -203 -60q-82 -15 -198 -11l-197 4q-84 2 -298 -11q-33 -3 -272 -11z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M0 -126l17 85q4 1 77 20q76 19 116 39q29 37 41 101l27 139l56 268l12 64q8 44 17 84.5t16 67t12.5 46.5t9 30.5t3.5 11.5l29 157l16 63l22 135l8 50v38q-41 22 -144 28q-28 2 -38 4l19 103l317 -14q39 -2 73 -2q66 0 214 9q33 2 68 4.5t36 2.5q-2 -19 -6 -38 q-7 -29 -13 -51q-55 -19 -109 -31q-64 -16 -101 -31q-12 -31 -24 -88q-9 -44 -13 -82q-44 -199 -66 -306l-61 -311l-38 -158l-43 -235l-12 -45q-2 -7 1 -27q64 -15 119 -21q36 -5 66 -10q-1 -29 -7 -58q-7 -31 -9 -41q-18 0 -23 -1q-24 -2 -42 -2q-9 0 -28 3q-19 4 -145 17 l-198 2q-41 1 -174 -11q-74 -7 -98 -9z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M81 1407l54 -27q20 -5 211 -5h130l19 3l115 1l215 -1h293l34 -2q14 -1 28 7t21 16l7 8l42 1q15 0 28 -1v-104.5t1 -131.5l1 -100l-1 -58q0 -32 -4 -51q-39 -15 -68 -18q-25 43 -54 128q-8 24 -15.5 62.5t-11.5 65.5t-6 29q-13 15 -27 19q-7 2 -42.5 2t-103.5 -1t-111 -1 q-34 0 -67 -5q-10 -97 -8 -136l1 -152v-332l3 -359l-1 -147q-1 -46 11 -85q49 -25 89 -32q2 0 18 -5t44 -13t43 -12q30 -8 50 -18q5 -45 5 -50q0 -10 -3 -29q-14 -1 -34 -1q-110 0 -187 10q-72 8 -238 8q-88 0 -233 -14q-48 -4 -70 -4q-2 22 -2 26l-1 26v9q21 33 79 49 q139 38 159 50q9 21 12 56q8 192 6 433l-5 428q-1 62 -0.5 118.5t0.5 102.5t-2 57t-6 15q-6 5 -14 6q-38 6 -148 6q-43 0 -100 -13.5t-73 -24.5q-13 -9 -22 -33t-22 -75t-24 -84q-6 -19 -19.5 -32t-20.5 -13q-44 27 -56 44v297v86zM1744 128q33 0 42 -18.5t-11 -44.5 l-126 -162q-20 -26 -49 -26t-49 26l-126 162q-20 26 -11 44.5t42 18.5h80v1024h-80q-33 0 -42 18.5t11 44.5l126 162q20 26 49 26t49 -26l126 -162q20 -26 11 -44.5t-42 -18.5h-80v-1024h80z" />
|
||||
<glyph unicode="" d="M81 1407l54 -27q20 -5 211 -5h130l19 3l115 1l446 -1h318l34 -2q14 -1 28 7t21 16l7 8l42 1q15 0 28 -1v-104.5t1 -131.5l1 -100l-1 -58q0 -32 -4 -51q-39 -15 -68 -18q-25 43 -54 128q-8 24 -15.5 62.5t-11.5 65.5t-6 29q-13 15 -27 19q-7 2 -58.5 2t-138.5 -1t-128 -1 q-94 0 -127 -5q-10 -97 -8 -136l1 -152v52l3 -359l-1 -147q-1 -46 11 -85q49 -25 89 -32q2 0 18 -5t44 -13t43 -12q30 -8 50 -18q5 -45 5 -50q0 -10 -3 -29q-14 -1 -34 -1q-110 0 -187 10q-72 8 -238 8q-82 0 -233 -13q-45 -5 -70 -5q-2 22 -2 26l-1 26v9q21 33 79 49 q139 38 159 50q9 21 12 56q6 137 6 433l-5 44q0 265 -2 278q-2 11 -6 15q-6 5 -14 6q-38 6 -148 6q-50 0 -168.5 -14t-132.5 -24q-13 -9 -22 -33t-22 -75t-24 -84q-6 -19 -19.5 -32t-20.5 -13q-44 27 -56 44v297v86zM1505 113q26 -20 26 -49t-26 -49l-162 -126 q-26 -20 -44.5 -11t-18.5 42v80h-1024v-80q0 -33 -18.5 -42t-44.5 11l-162 126q-26 20 -26 49t26 49l162 126q26 20 44.5 11t18.5 -42v-80h1024v80q0 33 18.5 42t44.5 -11z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M725 977l-170 -450q33 0 136.5 -2t160.5 -2q19 0 57 2q-87 253 -184 452zM0 -128l2 79q23 7 56 12.5t57 10.5t49.5 14.5t44.5 29t31 50.5l237 616l280 724h75h53q8 -14 11 -21l205 -480q33 -78 106 -257.5t114 -274.5q15 -34 58 -144.5t72 -168.5q20 -45 35 -57 q19 -15 88 -29.5t84 -20.5q6 -38 6 -57q0 -4 -0.5 -13t-0.5 -13q-63 0 -190 8t-191 8q-76 0 -215 -7t-178 -8q0 43 4 78l131 28q1 0 12.5 2.5t15.5 3.5t14.5 4.5t15 6.5t11 8t9 11t2.5 14q0 16 -31 96.5t-72 177.5t-42 100l-450 2q-26 -58 -76.5 -195.5t-50.5 -162.5 q0 -22 14 -37.5t43.5 -24.5t48.5 -13.5t57 -8.5t41 -4q1 -19 1 -58q0 -9 -2 -27q-58 0 -174.5 10t-174.5 10q-8 0 -26.5 -4t-21.5 -4q-80 -14 -188 -14z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M555 15q74 -32 140 -32q376 0 376 335q0 114 -41 180q-27 44 -61.5 74t-67.5 46.5t-80.5 25t-84 10.5t-94.5 2q-73 0 -101 -10q0 -53 -0.5 -159t-0.5 -158q0 -8 -1 -67.5t-0.5 -96.5t4.5 -83.5t12 -66.5zM541 761q42 -7 109 -7q82 0 143 13t110 44.5t74.5 89.5t25.5 142 q0 70 -29 122.5t-79 82t-108 43.5t-124 14q-50 0 -130 -13q0 -50 4 -151t4 -152q0 -27 -0.5 -80t-0.5 -79q0 -46 1 -69zM0 -128l2 94q15 4 85 16t106 27q7 12 12.5 27t8.5 33.5t5.5 32.5t3 37.5t0.5 34v35.5v30q0 982 -22 1025q-4 8 -22 14.5t-44.5 11t-49.5 7t-48.5 4.5 t-30.5 3l-4 83q98 2 340 11.5t373 9.5q23 0 68.5 -0.5t67.5 -0.5q70 0 136.5 -13t128.5 -42t108 -71t74 -104.5t28 -137.5q0 -52 -16.5 -95.5t-39 -72t-64.5 -57.5t-73 -45t-84 -40q154 -35 256.5 -134t102.5 -248q0 -100 -35 -179.5t-93.5 -130.5t-138 -85.5t-163.5 -48.5 t-176 -14q-44 0 -132 3t-132 3q-106 0 -307 -11t-231 -12z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M0 -126l17 85q6 2 81.5 21.5t111.5 37.5q28 35 41 101q1 7 62 289t114 543.5t52 296.5v25q-24 13 -54.5 18.5t-69.5 8t-58 5.5l19 103q33 -2 120 -6.5t149.5 -7t120.5 -2.5q48 0 98.5 2.5t121 7t98.5 6.5q-5 -39 -19 -89q-30 -10 -101.5 -28.5t-108.5 -33.5 q-8 -19 -14 -42.5t-9 -40t-7.5 -45.5t-6.5 -42q-27 -148 -87.5 -419.5t-77.5 -355.5q-2 -9 -13 -58t-20 -90t-16 -83.5t-6 -57.5l1 -18q17 -4 185 -31q-3 -44 -16 -99q-11 0 -32.5 -1.5t-32.5 -1.5q-29 0 -87 10t-86 10q-138 2 -206 2q-51 0 -143 -9t-121 -11z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1744 128q33 0 42 -18.5t-11 -44.5l-126 -162q-20 -26 -49 -26t-49 26l-126 162q-20 26 -11 44.5t42 18.5h80v1024h-80q-33 0 -42 18.5t11 44.5l126 162q20 26 49 26t49 -26l126 -162q20 -26 11 -44.5t-42 -18.5h-80v-1024h80zM81 1407l54 -27q12 -5 211 -5q44 0 132 2 t132 2q36 0 107.5 -0.5t107.5 -0.5h293q6 0 21 -0.5t20.5 0t16 3t17.5 9t15 17.5l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 48t-14.5 73.5t-7.5 35.5q-6 8 -12 12.5t-15.5 6t-13 2.5t-18 0.5t-16.5 -0.5 q-17 0 -66.5 0.5t-74.5 0.5t-64 -2t-71 -6q-9 -81 -8 -136q0 -94 2 -388t2 -455q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q19 42 19 383q0 101 -3 303t-3 303v117q0 2 0.5 15.5t0.5 25t-1 25.5t-3 24t-5 14q-11 12 -162 12q-33 0 -93 -12t-80 -26q-19 -13 -34 -72.5t-31.5 -111t-42.5 -53.5q-42 26 -56 44v383z" />
|
||||
<glyph unicode="" d="M81 1407l54 -27q12 -5 211 -5q44 0 132 2t132 2q70 0 246.5 1t304.5 0.5t247 -4.5q33 -1 56 31l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 47.5t-15 73.5t-7 36q-10 13 -27 19q-5 2 -66 2q-30 0 -93 1t-103 1 t-94 -2t-96 -7q-9 -81 -8 -136l1 -152v52q0 -55 1 -154t1.5 -180t0.5 -153q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q7 16 11.5 74t6 145.5t1.5 155t-0.5 153.5t-0.5 89q0 7 -2.5 21.5t-2.5 22.5q0 7 0.5 44t1 73t0 76.5t-3 67.5t-6.5 32q-11 12 -162 12q-41 0 -163 -13.5t-138 -24.5q-19 -12 -34 -71.5t-31.5 -111.5t-42.5 -54q-42 26 -56 44v383zM1310 125q12 0 42 -19.5t57.5 -41.5 t59.5 -49t36 -30q26 -21 26 -49t-26 -49q-4 -3 -36 -30t-59.5 -49t-57.5 -41.5t-42 -19.5q-13 0 -20.5 10.5t-10 28.5t-2.5 33.5t1.5 33t1.5 19.5h-1024q0 -2 1.5 -19.5t1.5 -33t-2.5 -33.5t-10 -28.5t-20.5 -10.5q-12 0 -42 19.5t-57.5 41.5t-59.5 49t-36 30q-26 21 -26 49 t26 49q4 3 36 30t59.5 49t57.5 41.5t42 19.5q13 0 20.5 -10.5t10 -28.5t2.5 -33.5t-1.5 -33t-1.5 -19.5h1024q0 2 -1.5 19.5t-1.5 33t2.5 33.5t10 28.5t20.5 10.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45t-45 -19 h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h640q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
|
||||
@@ -109,8 +110,8 @@
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19l-710 -710 q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19z" />
|
||||
<glyph unicode="" horiz-adv-x="1538" d="M14 557l710 710q19 19 45 19t45 -19l710 -710q19 -19 13 -32t-32 -13h-1472q-26 0 -32 13t13 32zM1473 0h-1408q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1408q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19z" />
|
||||
<glyph unicode="" horiz-adv-x="1152" d="M742 -37l-652 651q-37 37 -37 90.5t37 90.5l652 651q37 37 90.5 37t90.5 -37l75 -75q37 -37 37 -90.5t-37 -90.5l-486 -486l486 -485q37 -38 37 -91t-37 -90l-75 -75q-37 -37 -90.5 -37t-90.5 37z" />
|
||||
<glyph unicode="" horiz-adv-x="1152" d="M1099 704q0 -52 -37 -91l-652 -651q-37 -37 -90 -37t-90 37l-76 75q-37 39 -37 91q0 53 37 90l486 486l-486 485q-37 39 -37 91q0 53 37 90l76 75q36 38 90 38t90 -38l652 -651q37 -37 37 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1171 1235l-531 -531l531 -531q19 -19 19 -45t-19 -45l-166 -166q-19 -19 -45 -19t-45 19l-742 742q-19 19 -19 45t19 45l742 742q19 19 45 19t45 -19l166 -166q19 -19 19 -45t-19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1107 659l-742 -742q-19 -19 -45 -19t-45 19l-166 166q-19 19 -19 45t19 45l531 531l-531 531q-19 19 -19 45t19 45l166 166q19 19 45 19t45 -19l742 -742q19 -19 19 -45t-19 -45z" />
|
||||
<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-256v256q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-256h-256q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h256v-256q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v256h256q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5 t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
|
||||
<glyph unicode="" d="M1149 414q0 26 -19 45l-181 181l181 181q19 19 19 45q0 27 -19 46l-90 90q-19 19 -46 19q-26 0 -45 -19l-181 -181l-181 181q-19 19 -45 19q-27 0 -46 -19l-90 -90q-19 -19 -19 -46q0 -26 19 -45l181 -181l-181 -181q-19 -19 -19 -45q0 -27 19 -46l90 -90q19 -19 46 -19 q26 0 45 19l181 181l181 -181q19 -19 45 -19q27 0 46 19l90 90q19 19 19 46zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
@@ -143,17 +144,17 @@
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M666 1055q-60 -92 -137 -273q-22 45 -37 72.5t-40.5 63.5t-51 56.5t-63 35t-81.5 14.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q250 0 410 -225zM1792 256q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192q-32 0 -85 -0.5t-81 -1t-73 1 t-71 5t-64 10.5t-63 18.5t-58 28.5t-59 40t-55 53.5t-56 69.5q59 93 136 273q22 -45 37 -72.5t40.5 -63.5t51 -56.5t63 -35t81.5 -14.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1792 1152q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5 v192h-256q-48 0 -87 -15t-69 -45t-51 -61.5t-45 -77.5q-32 -62 -78 -171q-29 -66 -49.5 -111t-54 -105t-64 -100t-74 -83t-90 -68.5t-106.5 -42t-128 -16.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q48 0 87 15t69 45t51 61.5t45 77.5q32 62 78 171q29 66 49.5 111 t54 105t64 100t74 83t90 68.5t106.5 42t128 16.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22q-17 -2 -30.5 9t-17.5 29v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281 q0 130 71 248.5t191 204.5t286 136.5t348 50.5q244 0 450 -85.5t326 -233t120 -321.5z" />
|
||||
<glyph unicode="" d="M1536 704v-128q0 -201 -98.5 -362t-274 -251.5t-395.5 -90.5t-395.5 90.5t-274 251.5t-98.5 362v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-128q0 -52 23.5 -90t53.5 -57t71 -30t64 -13t44 -2t44 2t64 13t71 30t53.5 57t23.5 90v128q0 26 19 45t45 19h384 q26 0 45 -19t19 -45zM512 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45zM1536 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1611 320q0 -53 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-486 485l-486 -485q-36 -38 -90 -38t-90 38l-75 75q-38 36 -38 90q0 53 38 91l651 651q37 37 90 37q52 0 91 -37l650 -651q38 -38 38 -91z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1611 832q0 -53 -37 -90l-651 -651q-38 -38 -91 -38q-54 0 -90 38l-651 651q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l486 -486l486 486q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1683 205l-166 -165q-19 -19 -45 -19t-45 19l-531 531l-531 -531q-19 -19 -45 -19t-45 19l-166 165q-19 19 -19 45.5t19 45.5l742 741q19 19 45 19t45 -19l742 -741q19 -19 19 -45.5t-19 -45.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1683 728l-742 -741q-19 -19 -45 -19t-45 19l-742 741q-19 19 -19 45.5t19 45.5l166 165q19 19 45 19t45 -19l531 -531l531 531q19 19 45 19t45 -19l166 -165q19 -19 19 -45.5t-19 -45.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5 -8t3 -11.5t1 -11.5v-13v-11v-160v-416h192q26 0 45 -19t19 -45z " />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5 l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5 t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M640 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1536 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1664 1088v-512q0 -24 -16.5 -42.5t-40.5 -21.5l-1044 -122q13 -60 13 -70q0 -16 -24 -64h920q26 0 45 -19t19 -45 t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 11 8 31.5t16 36t21.5 40t15.5 29.5l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t19.5 -15.5t13 -24.5t8 -26t5.5 -29.5t4.5 -26h1201q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" />
|
||||
<glyph unicode="" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M512 512v-384h-256v384h256zM896 1024v-896h-256v896h256zM1280 768v-640h-256v640h256zM1664 1152v-1024h-256v1024h256zM1792 32v1216q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5z M1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" />
|
||||
<glyph unicode="" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5 t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M1307 618l23 219h-198v109q0 49 15.5 68.5t71.5 19.5h110v219h-175q-152 0 -218 -72t-66 -213v-131h-131v-219h131v-635h262v635h175zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-188v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-532q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 -90.5t-90.5 -37.5h-1536q-53 0 -90.5 37.5t-37.5 90.5v1280 q0 53 37.5 90.5t90.5 37.5h1536q53 0 90.5 -37.5t37.5 -90.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189 -131 -365l355 -355l96 96q-3 3 -26 24.5t-40 38.5t-33 36.5 t-16 28.5q0 17 49 66t66 49q13 0 23 -10q6 -6 46 -44.5t82 -79.5t86.5 -86t73 -78t28.5 -41z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11 -108 -23 -155q-7 -24 -30 -24h-186q-11 0 -20 7.5t-10 17.5 l-23 153q-34 10 -75 31l-118 -89q-7 -7 -20 -7q-11 0 -21 8q-144 133 -144 160q0 9 7 19q10 14 41 53t47 61q-23 44 -35 82l-152 24q-10 1 -17 9.5t-7 19.5v185q0 10 7 19.5t16 10.5l155 24q11 35 32 76q-34 48 -90 115q-7 11 -7 20q0 12 7 20q22 30 82 89t79 59q11 0 21 -7 l115 -90q34 18 77 32q11 108 23 154q7 24 30 24h186q11 0 20 -7.5t10 -17.5l23 -153q34 -10 75 -31l118 89q8 7 20 7q11 0 21 -8q144 -133 144 -160q0 -9 -7 -19q-12 -16 -42 -54t-45 -60q23 -48 34 -82l152 -23q10 -2 17 -10.5t7 -19.5zM1920 198v-140q0 -16 -149 -31 q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20 t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31zM1920 1222v-140q0 -16 -149 -31q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68 q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70 q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31z" />
|
||||
@@ -168,7 +169,7 @@
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" />
|
||||
<glyph unicode="" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M519 336q4 6 -3 13q-9 7 -14 2q-4 -6 3 -13q9 -7 14 -2zM491 377q-5 7 -12 4q-6 -4 0 -12q7 -8 12 -5q6 4 0 13zM450 417q2 4 -5 8q-7 2 -8 -2q-3 -5 4 -8q8 -2 9 2zM471 394q2 1 1.5 4.5t-3.5 5.5q-6 7 -10 3t1 -11q6 -6 11 -2zM557 319q2 7 -9 11q-9 3 -13 -4 q-2 -7 9 -11q9 -3 13 4zM599 316q0 8 -12 8q-10 0 -10 -8t11 -8t11 8zM638 323q-2 7 -13 5t-9 -9q2 -8 12 -6t10 10zM1280 640q0 212 -150 362t-362 150t-362 -150t-150 -362q0 -167 98 -300.5t252 -185.5q18 -3 26.5 5t8.5 20q0 52 -1 95q-6 -1 -15.5 -2.5t-35.5 -2t-48 4 t-43.5 20t-29.5 41.5q-23 59 -57 74q-2 1 -4.5 3.5l-8 8t-7 9.5t4 7.5t19.5 3.5q6 0 15 -2t30 -15.5t33 -35.5q16 -28 37.5 -42t43.5 -14t38 3.5t30 9.5q7 47 33 69q-49 6 -86 18.5t-73 39t-55.5 76t-19.5 119.5q0 79 53 137q-24 62 5 136q19 6 54.5 -7.5t60.5 -29.5l26 -16 q58 17 128 17t128 -17q11 7 28.5 18t55.5 26t57 9q29 -74 5 -136q53 -58 53 -137q0 -57 -14 -100.5t-35.5 -70t-53.5 -44.5t-62.5 -26t-68.5 -12q35 -31 35 -95q0 -40 -0.5 -89t-0.5 -51q0 -12 8.5 -20t26.5 -5q154 52 252 185.5t98 300.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" />
|
||||
<glyph unicode="" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" />
|
||||
@@ -176,14 +177,14 @@
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289 q0 34 19.5 62t52.5 41q21 9 44 9h1048z" />
|
||||
<glyph unicode="" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" />
|
||||
<glyph unicode="" horiz-adv-x="768" d="M511 980h257l-30 -284h-227v-824h-341v824h-170v284h170v171q0 182 86 275.5t283 93.5h227v-284h-142q-39 0 -62.5 -6.5t-34 -23.5t-13.5 -34.5t-3 -49.5v-142z" />
|
||||
<glyph unicode="" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M959 1524v-264h-157q-86 0 -116 -36t-30 -108v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218q0 186 104 288.5t277 102.5q147 0 228 -12z" />
|
||||
<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -40 7t-13 30q0 3 0.5 76.5t0.5 134.5q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 119 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24 q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-85 13.5q-45 -113 -8 -204q-79 -87 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-39 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5 t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -88.5t0.5 -54.5q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103zM291 305q3 7 -7 12 q-10 3 -13 -2q-3 -7 7 -12q9 -6 13 2zM322 271q7 5 -2 16q-10 9 -16 3q-7 -5 2 -16q10 -10 16 -3zM352 226q9 7 0 19q-8 13 -17 6q-9 -5 0 -18t17 -7zM394 184q8 8 -4 19q-12 12 -20 3q-9 -8 4 -19q12 -12 20 -3zM451 159q3 11 -13 16q-15 4 -19 -7t13 -15q15 -6 19 6z M514 154q0 13 -17 11q-16 0 -16 -11q0 -13 17 -11q16 0 16 11zM572 164q-2 11 -18 9q-16 -3 -14 -15t18 -8t14 14z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" />
|
||||
<glyph unicode="" d="M1040 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1296 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1408 160v320q0 13 -9.5 22.5t-22.5 9.5 h-1216q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h1216q13 0 22.5 9.5t9.5 22.5zM178 640h1180l-157 482q-4 13 -16 21.5t-26 8.5h-782q-14 0 -26 -8.5t-16 -21.5zM1536 480v-320q0 -66 -47 -113t-113 -47h-1216q-66 0 -113 47t-47 113v320q0 25 16 75 l197 606q17 53 63 86t101 33h782q55 0 101 -33t63 -86l197 -606q16 -50 16 -75z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1664 896q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5v-384q0 -52 -38 -90t-90 -38q-417 347 -812 380q-58 -19 -91 -66t-31 -100.5t40 -92.5q-20 -33 -23 -65.5t6 -58t33.5 -55t48 -50t61.5 -50.5q-29 -58 -111.5 -83t-168.5 -11.5t-132 55.5q-7 23 -29.5 87.5 t-32 94.5t-23 89t-15 101t3.5 98.5t22 110.5h-122q-66 0 -113 47t-47 113v192q0 66 47 113t113 47h480q435 0 896 384q52 0 90 -38t38 -90v-384zM1536 292v954q-394 -302 -768 -343v-270q377 -42 768 -341z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M848 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM183 128h1298q-164 181 -246.5 411.5t-82.5 484.5q0 256 -320 256t-320 -256q0 -254 -82.5 -484.5t-246.5 -411.5zM1664 128q0 -52 -38 -90t-90 -38 h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q190 161 287 397.5t97 498.5q0 165 96 262t264 117q-8 18 -8 37q0 40 28 68t68 28t68 -28t28 -68q0 -19 -8 -37q168 -20 264 -117t96 -262q0 -262 97 -498.5t287 -397.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM246 128h1300q-266 300 -266 832q0 51 -24 105t-69 103t-121.5 80.5t-169.5 31.5t-169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -532 -266 -832z M1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5 t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" />
|
||||
<glyph unicode="" d="M1376 640l138 -135q30 -28 20 -70q-12 -41 -52 -51l-188 -48l53 -186q12 -41 -19 -70q-29 -31 -70 -19l-186 53l-48 -188q-10 -40 -51 -52q-12 -2 -19 -2q-31 0 -51 22l-135 138l-135 -138q-28 -30 -70 -20q-41 11 -51 52l-48 188l-186 -53q-41 -12 -70 19q-31 29 -19 70 l53 186l-188 48q-40 10 -52 51q-10 42 20 70l138 135l-138 135q-30 28 -20 70q12 41 52 51l188 48l-53 186q-12 41 19 70q29 31 70 19l186 -53l48 188q10 41 51 51q41 12 70 -19l135 -139l135 139q29 30 70 19q41 -10 51 -51l48 -188l186 53q41 12 70 -19q31 -29 19 -70 l-53 -186l188 -48q40 -10 52 -51q10 -42 -20 -70z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 768q0 51 -39 89.5t-89 38.5h-576q0 20 15 48.5t33 55t33 68t15 84.5q0 67 -44.5 97.5t-115.5 30.5q-24 0 -90 -139q-24 -44 -37 -65q-40 -64 -112 -145q-71 -81 -101 -106 q-69 -57 -140 -57h-32v-640h32q72 0 167 -32t193.5 -64t179.5 -32q189 0 189 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5h331q52 0 90 38t38 90zM1792 769q0 -105 -75.5 -181t-180.5 -76h-169q-4 -62 -37 -119q3 -21 3 -43 q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5q-133 0 -322 69q-164 59 -223 59h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h288q10 0 21.5 4.5t23.5 14t22.5 18t24 22.5t20.5 21.5t19 21.5t14 17q65 74 100 129q13 21 33 62t37 72t40.5 63t55 49.5 t69.5 17.5q125 0 206.5 -67t81.5 -189q0 -68 -22 -128h374q104 0 180 -76t76 -179z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1376 128h32v640h-32q-35 0 -67.5 12t-62.5 37t-50 46t-49 54q-2 3 -3.5 4.5t-4 4.5t-4.5 5q-72 81 -112 145q-14 22 -38 68q-1 3 -10.5 22.5t-18.5 36t-20 35.5t-21.5 30.5t-18.5 11.5q-71 0 -115.5 -30.5t-44.5 -97.5q0 -43 15 -84.5t33 -68t33 -55t15 -48.5h-576 q-50 0 -89 -38.5t-39 -89.5q0 -52 38 -90t90 -38h331q-15 -17 -25 -47.5t-10 -55.5q0 -69 53 -119q-18 -32 -18 -69t17.5 -73.5t47.5 -52.5q-4 -24 -4 -56q0 -85 48.5 -126t135.5 -41q84 0 183 32t194 64t167 32zM1664 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45 t45 -19t45 19t19 45zM1792 768v-640q0 -53 -37.5 -90.5t-90.5 -37.5h-288q-59 0 -223 -59q-190 -69 -317 -69q-142 0 -230 77.5t-87 217.5l1 5q-61 76 -61 178q0 22 3 43q-33 57 -37 119h-169q-105 0 -180.5 76t-75.5 181q0 103 76 179t180 76h374q-22 60 -22 128 q0 122 81.5 189t206.5 67q38 0 69.5 -17.5t55 -49.5t40.5 -63t37 -72t33 -62q35 -55 100 -129q2 -3 14 -17t19 -21.5t20.5 -21.5t24 -22.5t22.5 -18t23.5 -14t21.5 -4.5h288q53 0 90.5 -37.5t37.5 -90.5z" />
|
||||
@@ -218,8 +219,8 @@
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5 18.5t-4 26.5q0 26 19 45t45 19v320q0 8 -0.5 35t0 38 t2.5 34.5t6.5 37t14 30.5t22.5 30l198 198q19 19 50.5 32t58.5 13h160v192q0 26 19 45t45 19h1024q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11.5 75.5q0 50 -27 83t-77 33 q-62 0 -105 -57t-43 -142q0 -73 25 -122l-99 -418q-17 -70 -13 -177q-206 91 -333 281t-127 423q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 -97 43.5t-23 103.5q8 34 26.5 92.5 t29.5 102t11 74.5q0 49 -26.5 81.5t-75.5 32.5q-61 0 -103.5 -56.5t-42.5 -139.5q0 -72 24 -121l-98 -414q-24 -100 -7 -254h-183q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960z" />
|
||||
<glyph unicode="" d="M678 -57q0 -38 -10 -71h-380q-95 0 -171.5 56.5t-103.5 147.5q24 45 69 77.5t100 49.5t107 24t107 7q32 0 49 -2q6 -4 30.5 -21t33 -23t31 -23t32 -25.5t27.5 -25.5t26.5 -29.5t21 -30.5t17.5 -34.5t9.5 -36t4.5 -40.5zM385 294q-234 -7 -385 -85v433q103 -118 273 -118 q32 0 70 5q-21 -61 -21 -86q0 -67 63 -149zM558 805q0 -100 -43.5 -160.5t-140.5 -60.5q-51 0 -97 26t-78 67.5t-56 93.5t-35.5 104t-11.5 99q0 96 51.5 165t144.5 69q66 0 119 -41t84 -104t47 -130t16 -128zM1536 896v-736q0 -119 -84.5 -203.5t-203.5 -84.5h-468 q39 73 39 157q0 66 -22 122.5t-55.5 93t-72 71t-72 59.5t-55.5 54.5t-22 59.5q0 36 23 68t56 61.5t65.5 64.5t55.5 93t23 131t-26.5 145.5t-75.5 118.5q-6 6 -14 11t-12.5 7.5t-10 9.5t-10.5 17h135l135 64h-437q-138 0 -244.5 -38.5t-182.5 -133.5q0 126 81 213t207 87h960 q119 0 203.5 -84.5t84.5 -203.5v-96h-256v256h-128v-256h-256v-128h256v-256h128v256h256z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M876 71q0 21 -4.5 40.5t-9.5 36t-17.5 34.5t-21 30.5t-26.5 29.5t-27.5 25.5t-32 25.5t-31 23t-33 23t-30.5 21q-17 2 -50 2q-54 0 -106 -7t-108 -25t-98 -46t-69 -75t-27 -107q0 -68 35.5 -121.5t93 -84t120.5 -45.5t127 -15q59 0 112.5 12.5t100.5 39t74.5 73.5 t27.5 110zM756 933q0 60 -16.5 127.5t-47 130.5t-84 104t-119.5 41q-93 0 -144 -69t-51 -165q0 -47 11.5 -99t35.5 -104t56 -93.5t78 -67.5t97 -26q97 0 140.5 60.5t43.5 160.5zM625 1408h437l-135 -79h-135q71 -45 110 -126t39 -169q0 -74 -23 -131.5t-56 -92.5t-66 -64.5 t-56 -61t-23 -67.5q0 -26 16.5 -51t43 -48t58.5 -48t64 -55.5t58.5 -66t43 -85t16.5 -106.5q0 -160 -140 -282q-152 -131 -420 -131q-59 0 -119.5 10t-122 33.5t-108.5 58t-77 89t-30 121.5q0 61 37 135q32 64 96 110.5t145 71t155 36t150 13.5q-64 83 -64 149q0 12 2 23.5 t5 19.5t8 21.5t7 21.5q-40 -5 -70 -5q-149 0 -255.5 98t-106.5 246q0 140 95 250.5t234 141.5q94 20 187 20zM1664 1152v-128h-256v-256h-128v256h-256v128h256v256h128v-256h256z" />
|
||||
<glyph unicode="" d="M917 631q0 26 -6 64h-362v-132h217q-3 -24 -16.5 -50t-37.5 -53t-66.5 -44.5t-96.5 -17.5q-99 0 -169 71t-70 171t70 171t169 71q92 0 153 -59l104 101q-108 100 -257 100q-160 0 -272 -112.5t-112 -271.5t112 -271.5t272 -112.5q165 0 266.5 105t101.5 270zM1262 585 h109v110h-109v110h-110v-110h-110v-110h110v-110h110v110zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1437 623q0 -208 -87 -370.5t-248 -254t-369 -91.5q-149 0 -285 58t-234 156t-156 234t-58 285t58 285t156 234t234 156t285 58q286 0 491 -192l-199 -191q-117 113 -292 113q-123 0 -227.5 -62t-165.5 -168.5t-61 -232.5t61 -232.5t165.5 -168.5t227.5 -62 q83 0 152.5 23t114.5 57.5t78.5 78.5t49 83t21.5 74h-416v252h692q12 -63 12 -122zM2304 745v-210h-209v-209h-210v209h-209v210h209v209h210v-209h209z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45 19h1792q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
|
||||
@@ -247,10 +248,10 @@
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 68 5.5 131t24 138t47.5 132.5t81 103t120 60.5q-22 -52 -22 -120v-203q-58 -20 -93 -70t-35 -111q0 -80 56 -136t136 -56 t136 56t56 136q0 61 -35.5 111t-92.5 70v203q0 62 25 93q132 -104 295 -104t295 104q25 -31 25 -93v-64q-106 0 -181 -75t-75 -181v-89q-32 -29 -32 -71q0 -40 28 -68t68 -28t68 28t28 68q0 42 -32 71v89q0 52 38 90t90 38t90 -38t38 -90v-89q-32 -29 -32 -71q0 -40 28 -68 t68 -28t68 28t28 68q0 42 -32 71v89q0 68 -34.5 127.5t-93.5 93.5q0 10 0.5 42.5t0 48t-2.5 41.5t-7 47t-13 40q68 -15 120 -60.5t81 -103t47.5 -132.5t24 -138t5.5 -131zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M1280 832q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 832q0 -62 -35.5 -111t-92.5 -70v-395q0 -159 -131.5 -271.5t-316.5 -112.5t-316.5 112.5t-131.5 271.5v132q-164 20 -274 128t-110 252v512q0 26 19 45t45 19q6 0 16 -2q17 30 47 48 t65 18q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5q-33 0 -64 18v-402q0 -106 94 -181t226 -75t226 75t94 181v402q-31 -18 -64 -18q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5q35 0 65 -18t47 -48q10 2 16 2q26 0 45 -19t19 -45v-512q0 -144 -110 -252 t-274 -128v-132q0 -106 94 -181t226 -75t226 75t94 181v395q-57 21 -92.5 70t-35.5 111q0 80 56 136t136 56t136 -56t56 -136z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h64zM1408 1152v-1280h-1024v1280h128v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h128zM1792 928v-832q0 -92 -66 -158t-158 -66h-64v1280h64q92 0 158 -66 t66 -158z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M848 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1664 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q190 161 287 397.5t97 498.5 q0 165 96 262t264 117q-8 18 -8 37q0 40 28 68t68 28t68 -28t28 -68q0 -19 -8 -37q168 -20 264 -117t96 -262q0 -262 97 -498.5t287 -397.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5 t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1664 896q0 80 -56 136t-136 56h-64v-384h64q80 0 136 56t56 136zM0 128h1792q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM1856 896q0 -159 -112.5 -271.5t-271.5 -112.5h-64v-32q0 -92 -66 -158t-158 -66h-704q-92 0 -158 66t-66 158v736q0 26 19 45 t45 19h1152q159 0 271.5 -112.5t112.5 -271.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M640 1472v-640q0 -61 -35.5 -111t-92.5 -70v-779q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v779q-57 20 -92.5 70t-35.5 111v640q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45 t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45zM1408 1472v-1600q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v512h-224q-13 0 -22.5 9.5t-9.5 22.5v800q0 132 94 226t226 94h256q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1024 352v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704q14 0 23 -9t9 -23zM1024 608v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704q14 0 23 -9t9 -23zM128 0h1024v768h-416q-40 0 -68 28t-28 68v416h-512v-1280z M768 896h376q-10 29 -22 41l-313 313q-12 12 -41 22v-376zM1280 864v-896q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h640q40 0 88 -20t76 -48l312 -312q28 -28 48 -76t20 -88z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M384 736q0 14 9 23t23 9h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64zM1120 512q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704zM1120 256q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704 q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1536h-1152v-1536h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM1408 1472v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1152h-256v-32q0 -40 -28 -68t-68 -28h-448q-40 0 -68 28t-28 68v32h-256v-1152h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM896 1056v320q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-96h-128v96q0 13 -9.5 22.5 t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5v96h128v-96q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1408 1088v-1280q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1280q0 26 19 45t45 19h320 v288q0 40 28 68t68 28h448q40 0 68 -28t28 -68v-288h320q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M640 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM256 640h384v256h-158q-14 -2 -22 -9l-195 -195q-7 -12 -9 -22v-30zM1536 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1664 800v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM1920 1344v-1152 q0 -26 -19 -45t-45 -19h-192q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-128q-26 0 -45 19t-19 45t19 45t45 19v416q0 26 13 58t32 51l198 198q19 19 51 32t58 13h160v320q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
|
||||
@@ -274,7 +275,7 @@
|
||||
<glyph unicode="" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136z" />
|
||||
<glyph unicode="" horiz-adv-x="1568" d="M496 192q0 -60 -42.5 -102t-101.5 -42q-60 0 -102 42t-42 102t42 102t102 42q59 0 101.5 -42t42.5 -102zM928 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -66 -47 -113t-113 -47t-113 47t-47 113 t47 113t113 47t113 -47t47 -113zM1360 192q0 -46 -33 -79t-79 -33t-79 33t-33 79t33 79t79 33t79 -33t33 -79zM528 1088q0 -73 -51.5 -124.5t-124.5 -51.5t-124.5 51.5t-51.5 124.5t51.5 124.5t124.5 51.5t124.5 -51.5t51.5 -124.5zM992 1280q0 -80 -56 -136t-136 -56 t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1536 640q0 -40 -28 -68t-68 -28t-68 28t-28 68t28 68t68 28t68 -28t28 -68zM1328 1088q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M526 142q0 -53 -37.5 -90.5t-90.5 -37.5q-52 0 -90 38t-38 90q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 -64q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1522 142q0 -52 -38 -90t-90 -38q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM558 1138q0 -66 -47 -113t-113 -47t-113 47t-47 113t47 113t113 47t113 -47t47 -113z M1728 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1088 1344q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1618 1138q0 -93 -66 -158.5t-158 -65.5q-93 0 -158.5 65.5t-65.5 158.5 q0 92 65.5 158t158.5 66q92 0 158 -66t66 -158z" />
|
||||
<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M640 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1280 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1440 320 q0 120 -69 204t-187 84q-41 0 -195 -21q-71 -11 -157 -11t-157 11q-152 21 -195 21q-118 0 -187 -84t-69 -204q0 -88 32 -153.5t81 -103t122 -60t140 -29.5t149 -7h168q82 0 149 7t140 29.5t122 60t81 103t32 153.5zM1664 496q0 -207 -61 -331q-38 -77 -105.5 -133t-141 -86 t-170 -47.5t-171.5 -22t-167 -4.5q-78 0 -142 3t-147.5 12.5t-152.5 30t-137 51.5t-121 81t-86 115q-62 123 -62 331q0 237 136 396q-27 82 -27 170q0 116 51 218q108 0 190 -39.5t189 -123.5q147 35 309 35q148 0 280 -32q105 82 187 121t189 39q51 -102 51 -218 q0 -87 -27 -168q136 -160 136 -398z" />
|
||||
@@ -345,8 +346,8 @@
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1043 971q0 100 -65 162t-171 62h-320v-448h320q106 0 171 62t65 162zM1280 971q0 -193 -126.5 -315t-326.5 -122h-340v-118h505q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-505v-192q0 -14 -9.5 -23t-22.5 -9h-167q-14 0 -23 9t-9 23v192h-224q-14 0 -23 9t-9 23v128 q0 14 9 23t23 9h224v118h-224q-14 0 -23 9t-9 23v149q0 13 9 22.5t23 9.5h224v629q0 14 9 23t23 9h539q200 0 326.5 -122t126.5 -315z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M514 341l81 299h-159l75 -300q1 -1 1 -3t1 -3q0 1 0.5 3.5t0.5 3.5zM630 768l35 128h-292l32 -128h225zM822 768h139l-35 128h-70zM1271 340l78 300h-162l81 -299q0 -1 0.5 -3.5t1.5 -3.5q0 1 0.5 3t0.5 3zM1382 768l33 128h-297l34 -128h230zM1792 736v-64q0 -14 -9 -23 t-23 -9h-213l-164 -616q-7 -24 -31 -24h-159q-24 0 -31 24l-166 616h-209l-167 -616q-7 -24 -31 -24h-159q-11 0 -19.5 7t-10.5 17l-160 616h-208q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h175l-33 128h-142q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h109l-89 344q-5 15 5 28 q10 12 26 12h137q26 0 31 -24l90 -360h359l97 360q7 24 31 24h126q24 0 31 -24l98 -360h365l93 360q5 24 31 24h137q16 0 26 -12q10 -13 5 -28l-91 -344h111q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-145l-34 -128h179q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1167 896q18 -182 -131 -258q117 -28 175 -103t45 -214q-7 -71 -32.5 -125t-64.5 -89t-97 -58.5t-121.5 -34.5t-145.5 -15v-255h-154v251q-80 0 -122 1v-252h-154v255q-18 0 -54 0.5t-55 0.5h-200l31 183h111q50 0 58 51v402h16q-6 1 -16 1v287q-13 68 -89 68h-111v164 l212 -1q64 0 97 1v252h154v-247q82 2 122 2v245h154v-252q79 -7 140 -22.5t113 -45t82.5 -78t36.5 -114.5zM952 351q0 36 -15 64t-37 46t-57.5 30.5t-65.5 18.5t-74 9t-69 3t-64.5 -1t-47.5 -1v-338q8 0 37 -0.5t48 -0.5t53 1.5t58.5 4t57 8.5t55.5 14t47.5 21t39.5 30 t24.5 40t9.5 51zM881 827q0 33 -12.5 58.5t-30.5 42t-48 28t-55 16.5t-61.5 8t-58 2.5t-54 -1t-39.5 -0.5v-307q5 0 34.5 -0.5t46.5 0t50 2t55 5.5t51.5 11t48.5 18.5t37 27t27 38.5t9 51z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1280 768v-800q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h544v-544q0 -40 28 -68t68 -28h544zM1277 896h-509v509q82 -15 132 -65l312 -312q50 -50 65 -132z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1024 160v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1024 416v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1280 768v-800q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28 t-28 68v1344q0 40 28 68t68 28h544v-544q0 -40 28 -68t68 -28h544zM1277 896h-509v509q82 -15 132 -65l312 -312q50 -50 65 -132z" />
|
||||
<glyph unicode="" d="M1024 1024v472q22 -14 36 -28l408 -408q14 -14 28 -36h-472zM896 992q0 -40 28 -68t68 -28h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544z" />
|
||||
<glyph unicode="" d="M1468 1060q14 -14 28 -36h-472v472q22 -14 36 -28zM992 896h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544q0 -40 28 -68t68 -28zM1152 160v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1191 1128h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1572 -23 v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -11v-2l14 2q9 2 30 2h248v119h121zM1661 874v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162 l230 -662h70z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1191 104h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1661 -150 v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70zM1572 1001v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -10v-3l14 3q9 1 30 1h248 v119h121z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1792 -32v-192q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832 q14 0 23 -9t9 -23zM1600 480v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1408 992v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1216 1504v-192q0 -14 -9 -23t-23 -9h-256 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23z" />
|
||||
@@ -361,14 +362,14 @@
|
||||
<glyph unicode="" d="M685 771q0 1 -126 222q-21 34 -52 34h-184q-18 0 -26 -11q-7 -12 1 -29l125 -216v-1l-196 -346q-9 -14 0 -28q8 -13 24 -13h185q31 0 50 36zM1309 1268q-7 12 -24 12h-187q-30 0 -49 -35l-411 -729q1 -2 262 -481q20 -35 52 -35h184q18 0 25 12q8 13 -1 28l-260 476v1 l409 723q8 16 0 28zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M928 135v-151l-707 -1v151zM1169 481v-701l-1 -35v-1h-1132l-35 1h-1v736h121v-618h928v618h120zM241 393l704 -65l-13 -150l-705 65zM309 709l683 -183l-39 -146l-683 183zM472 1058l609 -360l-77 -130l-609 360zM832 1389l398 -585l-124 -85l-399 584zM1285 1536 l121 -697l-149 -26l-121 697z" />
|
||||
<glyph unicode="" d="M1289 -96h-1118v480h-160v-640h1438v640h-160v-480zM347 428l33 157l783 -165l-33 -156zM450 802l67 146l725 -339l-67 -145zM651 1158l102 123l614 -513l-102 -123zM1048 1536l477 -641l-128 -96l-477 641zM330 65v159h800v-159h-800z" />
|
||||
<glyph unicode="" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-1142q-81 0 -139 58t-58 139v1142q0 81 58 139 t139 58h1142q81 0 139 -58t58 -139z" />
|
||||
<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" />
|
||||
<glyph unicode="" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q-86 -48 -189.5 -71.5t-202 -22t-201.5 18.5q-46 8 -81.5 18 t-76.5 27t-73 43.5t-52 61.5q-25 96 -57 292l6 16l18 9q223 -148 506.5 -148t507.5 148q21 -6 24 -23t-5 -45t-8 -37zM1403 1166q-26 -167 -111 -655q-5 -30 -27 -56t-43.5 -40t-54.5 -31q-252 -126 -610 -88q-248 27 -394 139q-15 12 -25.5 26.5t-17 35t-9 34t-6 39.5 t-5.5 35q-9 50 -26.5 150t-28 161.5t-23.5 147.5t-22 158q3 26 17.5 48.5t31.5 37.5t45 30t46 22.5t48 18.5q125 46 313 64q379 37 676 -50q155 -46 215 -122q16 -20 16.5 -51t-5.5 -54z" />
|
||||
<glyph unicode="" d="M848 666q0 43 -41 66t-77 1q-43 -20 -42.5 -72.5t43.5 -70.5q39 -23 81 4t36 72zM928 682q8 -66 -36 -121t-110 -61t-119 40t-56 113q-2 49 25.5 93t72.5 64q70 31 141.5 -10t81.5 -118zM1100 1073q-20 -21 -53.5 -34t-53 -16t-63.5 -8q-155 -20 -324 0q-44 6 -63 9.5 t-52.5 16t-54.5 32.5q13 19 36 31t40 15.5t47 8.5q198 35 408 1q33 -5 51 -8.5t43 -16t39 -31.5zM1142 327q0 7 5.5 26.5t3 32t-17.5 16.5q-161 -106 -365 -106t-366 106l-12 -6l-5 -12q26 -154 41 -210q47 -81 204 -108q249 -46 428 53q34 19 49 51.5t22.5 85.5t12.5 71z M1272 1020q9 53 -8 75q-43 55 -155 88q-216 63 -487 36q-132 -12 -226 -46q-38 -15 -59.5 -25t-47 -34t-29.5 -54q8 -68 19 -138t29 -171t24 -137q1 -5 5 -31t7 -36t12 -27t22 -28q105 -80 284 -100q259 -28 440 63q24 13 39.5 23t31 29t19.5 40q48 267 80 473zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M390 1408h219v-388h364v-241h-364v-394q0 -136 14 -172q13 -37 52 -60q50 -31 117 -31q117 0 232 76v-242q-102 -48 -178 -65q-77 -19 -173 -19q-105 0 -186 27q-78 25 -138 75q-58 51 -79 105q-22 54 -22 161v539h-170v217q91 30 155 84q64 55 103 132q39 78 54 196z " />
|
||||
<glyph unicode="" d="M1123 127v181q-88 -56 -174 -56q-51 0 -88 23q-29 17 -39 45q-11 30 -11 129v295h274v181h-274v291h-164q-11 -90 -40 -147t-78 -99q-48 -40 -116 -63v-163h127v-404q0 -78 17 -121q17 -42 59 -78q43 -37 104 -57q62 -20 140 -20q67 0 129 14q57 13 134 49zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M944 207l80 -237q-23 -35 -111 -66t-177 -32q-104 -2 -190.5 26t-142.5 74t-95 106t-55.5 120t-16.5 118v544h-168v215q72 26 129 69.5t91 90t58 102t34 99t15 88.5q1 5 4.5 8.5t7.5 3.5h244v-424h333v-252h-334v-518q0 -30 6.5 -56t22.5 -52.5t49.5 -41.5t81.5 -14 q78 2 134 29z" />
|
||||
<glyph unicode="" d="M1136 75l-62 183q-44 -22 -103 -22q-36 -1 -62 10.5t-38.5 31.5t-17.5 40.5t-5 43.5v398h257v194h-256v326h-188q-8 0 -9 -10q-5 -44 -17.5 -87t-39 -95t-77 -95t-118.5 -68v-165h130v-418q0 -57 21.5 -115t65 -111t121 -85.5t176.5 -30.5q69 1 136.5 25t85.5 50z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="768" d="M765 237q8 -19 -5 -35l-350 -384q-10 -10 -23 -10q-14 0 -24 10l-355 384q-13 16 -5 35q9 19 29 19h224v1248q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1248h224q21 0 29 -19z" />
|
||||
<glyph unicode="" horiz-adv-x="768" d="M765 1043q-9 -19 -29 -19h-224v-1248q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1248h-224q-21 0 -29 19t5 35l350 384q10 10 23 10q14 0 24 -10l355 -384q13 -16 5 -35z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 736v-192q0 -14 -9 -23t-23 -9h-1248v-224q0 -21 -19 -29t-35 5l-384 350q-10 10 -10 23q0 14 10 24l384 354q16 14 35 6q19 -9 19 -29v-224h1248q14 0 23 -9t9 -23z" />
|
||||
@@ -379,7 +380,7 @@
|
||||
<glyph unicode="" d="M663 1125q-11 -1 -15.5 -10.5t-8.5 -9.5q-5 -1 -5 5q0 12 19 15h10zM750 1111q-4 -1 -11.5 6.5t-17.5 4.5q24 11 32 -2q3 -6 -3 -9zM399 684q-4 1 -6 -3t-4.5 -12.5t-5.5 -13.5t-10 -13q-7 -10 -1 -12q4 -1 12.5 7t12.5 18q1 3 2 7t2 6t1.5 4.5t0.5 4v3t-1 2.5t-3 2z M1254 325q0 18 -55 42q4 15 7.5 27.5t5 26t3 21.5t0.5 22.5t-1 19.5t-3.5 22t-4 20.5t-5 25t-5.5 26.5q-10 48 -47 103t-72 75q24 -20 57 -83q87 -162 54 -278q-11 -40 -50 -42q-31 -4 -38.5 18.5t-8 83.5t-11.5 107q-9 39 -19.5 69t-19.5 45.5t-15.5 24.5t-13 15t-7.5 7 q-14 62 -31 103t-29.5 56t-23.5 33t-15 40q-4 21 6 53.5t4.5 49.5t-44.5 25q-15 3 -44.5 18t-35.5 16q-8 1 -11 26t8 51t36 27q37 3 51 -30t4 -58q-11 -19 -2 -26.5t30 -0.5q13 4 13 36v37q-5 30 -13.5 50t-21 30.5t-23.5 15t-27 7.5q-107 -8 -89 -134q0 -15 -1 -15 q-9 9 -29.5 10.5t-33 -0.5t-15.5 5q1 57 -16 90t-45 34q-27 1 -41.5 -27.5t-16.5 -59.5q-1 -15 3.5 -37t13 -37.5t15.5 -13.5q10 3 16 14q4 9 -7 8q-7 0 -15.5 14.5t-9.5 33.5q-1 22 9 37t34 14q17 0 27 -21t9.5 -39t-1.5 -22q-22 -15 -31 -29q-8 -12 -27.5 -23.5 t-20.5 -12.5q-13 -14 -15.5 -27t7.5 -18q14 -8 25 -19.5t16 -19t18.5 -13t35.5 -6.5q47 -2 102 15q2 1 23 7t34.5 10.5t29.5 13t21 17.5q9 14 20 8q5 -3 6.5 -8.5t-3 -12t-16.5 -9.5q-20 -6 -56.5 -21.5t-45.5 -19.5q-44 -19 -70 -23q-25 -5 -79 2q-10 2 -9 -2t17 -19 q25 -23 67 -22q17 1 36 7t36 14t33.5 17.5t30 17t24.5 12t17.5 2.5t8.5 -11q0 -2 -1 -4.5t-4 -5t-6 -4.5t-8.5 -5t-9 -4.5t-10 -5t-9.5 -4.5q-28 -14 -67.5 -44t-66.5 -43t-49 -1q-21 11 -63 73q-22 31 -25 22q-1 -3 -1 -10q0 -25 -15 -56.5t-29.5 -55.5t-21 -58t11.5 -63 q-23 -6 -62.5 -90t-47.5 -141q-2 -18 -1.5 -69t-5.5 -59q-8 -24 -29 -3q-32 31 -36 94q-2 28 4 56q4 19 -1 18l-4 -5q-36 -65 10 -166q5 -12 25 -28t24 -20q20 -23 104 -90.5t93 -76.5q16 -15 17.5 -38t-14 -43t-45.5 -23q8 -15 29 -44.5t28 -54t7 -70.5q46 24 7 92 q-4 8 -10.5 16t-9.5 12t-2 6q3 5 13 9.5t20 -2.5q46 -52 166 -36q133 15 177 87q23 38 34 30q12 -6 10 -52q-1 -25 -23 -92q-9 -23 -6 -37.5t24 -15.5q3 19 14.5 77t13.5 90q2 21 -6.5 73.5t-7.5 97t23 70.5q15 18 51 18q1 37 34.5 53t72.5 10.5t60 -22.5zM626 1152 q3 17 -2.5 30t-11.5 15q-9 2 -9 -7q2 -5 5 -6q10 0 7 -15q-3 -20 8 -20q3 0 3 3zM1045 955q-2 8 -6.5 11.5t-13 5t-14.5 5.5q-5 3 -9.5 8t-7 8t-5.5 6.5t-4 4t-4 -1.5q-14 -16 7 -43.5t39 -31.5q9 -1 14.5 8t3.5 20zM867 1168q0 11 -5 19.5t-11 12.5t-9 3q-14 -1 -7 -7l4 -2 q14 -4 18 -31q0 -3 8 2zM921 1401q0 2 -2.5 5t-9 7t-9.5 6q-15 15 -24 15q-9 -1 -11.5 -7.5t-1 -13t-0.5 -12.5q-1 -4 -6 -10.5t-6 -9t3 -8.5q4 -3 8 0t11 9t15 9q1 1 9 1t15 2t9 7zM1486 60q20 -12 31 -24.5t12 -24t-2.5 -22.5t-15.5 -22t-23.5 -19.5t-30 -18.5 t-31.5 -16.5t-32 -15.5t-27 -13q-38 -19 -85.5 -56t-75.5 -64q-17 -16 -68 -19.5t-89 14.5q-18 9 -29.5 23.5t-16.5 25.5t-22 19.5t-47 9.5q-44 1 -130 1q-19 0 -57 -1.5t-58 -2.5q-44 -1 -79.5 -15t-53.5 -30t-43.5 -28.5t-53.5 -11.5q-29 1 -111 31t-146 43q-19 4 -51 9.5 t-50 9t-39.5 9.5t-33.5 14.5t-17 19.5q-10 23 7 66.5t18 54.5q1 16 -4 40t-10 42.5t-4.5 36.5t10.5 27q14 12 57 14t60 12q30 18 42 35t12 51q21 -73 -32 -106q-32 -20 -83 -15q-34 3 -43 -10q-13 -15 5 -57q2 -6 8 -18t8.5 -18t4.5 -17t1 -22q0 -15 -17 -49t-14 -48 q3 -17 37 -26q20 -6 84.5 -18.5t99.5 -20.5q24 -6 74 -22t82.5 -23t55.5 -4q43 6 64.5 28t23 48t-7.5 58.5t-19 52t-20 36.5q-121 190 -169 242q-68 74 -113 40q-11 -9 -15 15q-3 16 -2 38q1 29 10 52t24 47t22 42q8 21 26.5 72t29.5 78t30 61t39 54q110 143 124 195 q-12 112 -16 310q-2 90 24 151.5t106 104.5q39 21 104 21q53 1 106 -13.5t89 -41.5q57 -42 91.5 -121.5t29.5 -147.5q-5 -95 30 -214q34 -113 133 -218q55 -59 99.5 -163t59.5 -191q8 -49 5 -84.5t-12 -55.5t-20 -22q-10 -2 -23.5 -19t-27 -35.5t-40.5 -33.5t-61 -14 q-18 1 -31.5 5t-22.5 13.5t-13.5 15.5t-11.5 20.5t-9 19.5q-22 37 -41 30t-28 -49t7 -97q20 -70 1 -195q-10 -65 18 -100.5t73 -33t85 35.5q59 49 89.5 66.5t103.5 42.5q53 18 77 36.5t18.5 34.5t-25 28.5t-51.5 23.5q-33 11 -49.5 48t-15 72.5t15.5 47.5q1 -31 8 -56.5 t14.5 -40.5t20.5 -28.5t21 -19t21.5 -13t16.5 -9.5z" />
|
||||
<glyph unicode="" d="M1024 36q-42 241 -140 498h-2l-2 -1q-16 -6 -43 -16.5t-101 -49t-137 -82t-131 -114.5t-103 -148l-15 11q184 -150 418 -150q132 0 256 52zM839 643q-21 49 -53 111q-311 -93 -673 -93q-1 -7 -1 -21q0 -124 44 -236.5t124 -201.5q50 89 123.5 166.5t142.5 124.5t130.5 81 t99.5 48l37 13q4 1 13 3.5t13 4.5zM732 855q-120 213 -244 378q-138 -65 -234 -186t-128 -272q302 0 606 80zM1416 536q-210 60 -409 29q87 -239 128 -469q111 75 185 189.5t96 250.5zM611 1277q-1 0 -2 -1q1 1 2 1zM1201 1132q-185 164 -433 164q-76 0 -155 -19 q131 -170 246 -382q69 26 130 60.5t96.5 61.5t65.5 57t37.5 40.5zM1424 647q-3 232 -149 410l-1 -1q-9 -12 -19 -24.5t-43.5 -44.5t-71 -60.5t-100 -65t-131.5 -64.5q25 -53 44 -95q2 -6 6.5 -17.5t7.5 -16.5q36 5 74.5 7t73.5 2t69 -1.5t64 -4t56.5 -5.5t48 -6.5t36.5 -6 t25 -4.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" d="M1173 473q0 50 -19.5 91.5t-48.5 68.5t-73 49t-82.5 34t-87.5 23l-104 24q-30 7 -44 10.5t-35 11.5t-30 16t-16.5 21t-7.5 30q0 77 144 77q43 0 77 -12t54 -28.5t38 -33.5t40 -29t48 -12q47 0 75.5 32t28.5 77q0 55 -56 99.5t-142 67.5t-182 23q-68 0 -132 -15.5 t-119.5 -47t-89 -87t-33.5 -128.5q0 -61 19 -106.5t56 -75.5t80 -48.5t103 -32.5l146 -36q90 -22 112 -36q32 -20 32 -60q0 -39 -40 -64.5t-105 -25.5q-51 0 -91.5 16t-65 38.5t-45.5 45t-46 38.5t-54 16q-50 0 -75.5 -30t-25.5 -75q0 -92 122 -157.5t291 -65.5 q73 0 140 18.5t122.5 53.5t88.5 93.5t33 131.5zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5q-130 0 -234 80q-77 -16 -150 -16q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5q0 73 16 150q-80 104 -80 234q0 159 112.5 271.5t271.5 112.5q130 0 234 -80 q77 16 150 16q143 0 273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -73 -16 -150q80 -104 80 -234z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1483 512l-587 -587q-52 -53 -127.5 -53t-128.5 53l-587 587q-53 53 -53 128t53 128l587 587q53 53 128 53t128 -53l265 -265l-398 -399l-188 188q-42 42 -99 42q-59 0 -100 -41l-120 -121q-42 -40 -42 -99q0 -58 42 -100l406 -408q30 -28 67 -37l6 -4h28q60 0 99 41 l619 619l2 -3q53 -53 53 -128t-53 -128zM1406 1138l120 -120q14 -15 14 -36t-14 -36l-730 -730q-17 -15 -37 -15v0q-4 0 -6 1q-18 2 -30 14l-407 408q-14 15 -14 36t14 35l121 120q13 15 35 15t36 -15l252 -252l574 575q15 15 36 15t36 -15z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1000 1102l37 194q5 23 -9 40t-35 17h-712q-23 0 -38.5 -17t-15.5 -37v-1101q0 -7 6 -1l291 352q23 26 38 33.5t48 7.5h239q22 0 37 14.5t18 29.5q24 130 37 191q4 21 -11.5 40t-36.5 19h-294q-29 0 -48 19t-19 48v42q0 29 19 47.5t48 18.5h346q18 0 35 13.5t20 29.5z M1227 1324q-15 -73 -53.5 -266.5t-69.5 -350t-35 -173.5q-6 -22 -9 -32.5t-14 -32.5t-24.5 -33t-38.5 -21t-58 -10h-271q-13 0 -22 -10q-8 -9 -426 -494q-22 -25 -58.5 -28.5t-48.5 5.5q-55 22 -55 98v1410q0 55 38 102.5t120 47.5h888q95 0 127 -53t10 -159zM1227 1324 l-158 -790q4 17 35 173.5t69.5 350t53.5 266.5z" />
|
||||
<glyph unicode="" d="M704 192v1024q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-1024q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1376 576v640q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-640q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408 q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1280 480q0 -40 -28 -68t-68 -28q-51 0 -80 43l-227 341h-45v-132l247 -411q9 -15 9 -33q0 -26 -19 -45t-45 -19h-192v-272q0 -46 -33 -79t-79 -33h-160q-46 0 -79 33t-33 79v272h-192q-26 0 -45 19t-19 45q0 18 9 33l247 411v132h-45l-227 -341q-29 -43 -80 -43 q-40 0 -68 28t-28 68q0 29 16 53l256 384q73 107 176 107h384q103 0 176 -107l256 -384q16 -24 16 -53zM864 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M1024 832v-416q0 -40 -28 -68t-68 -28t-68 28t-28 68v352h-64v-912q0 -46 -33 -79t-79 -33t-79 33t-33 79v464h-64v-464q0 -46 -33 -79t-79 -33t-79 33t-33 79v912h-64v-352q0 -40 -28 -68t-68 -28t-68 28t-28 68v416q0 80 56 136t136 56h640q80 0 136 -56t56 -136z M736 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" />
|
||||
@@ -398,17 +399,287 @@
|
||||
<glyph unicode="" d="M1024 960v-640q0 -26 -19 -45t-45 -19q-20 0 -37 12l-448 320q-27 19 -27 52t27 52l448 320q17 12 37 12q26 0 45 -19t19 -45zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5 t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1023 349l102 -204q-58 -179 -210 -290t-339 -111q-156 0 -288.5 77.5t-210 210t-77.5 288.5q0 181 104.5 330t274.5 211l17 -131q-122 -54 -195 -165.5t-73 -244.5q0 -185 131.5 -316.5t316.5 -131.5q126 0 232.5 65t165 175.5t49.5 236.5zM1571 249l58 -114l-256 -128 q-13 -7 -29 -7q-40 0 -57 35l-239 477h-472q-24 0 -42.5 16.5t-21.5 40.5l-96 779q-2 16 6 42q14 51 57 82.5t97 31.5q66 0 113 -47t47 -113q0 -69 -52 -117.5t-120 -41.5l37 -289h423v-128h-407l16 -128h455q40 0 57 -35l228 -455z" />
|
||||
<glyph unicode="" d="M1254 899q16 85 -21 132q-52 65 -187 45q-17 -3 -41 -12.5t-57.5 -30.5t-64.5 -48.5t-59.5 -70t-44.5 -91.5q80 7 113.5 -16t26.5 -99q-5 -52 -52 -143q-43 -78 -71 -99q-44 -32 -87 14q-23 24 -37.5 64.5t-19 73t-10 84t-8.5 71.5q-23 129 -34 164q-12 37 -35.5 69 t-50.5 40q-57 16 -127 -25q-54 -32 -136.5 -106t-122.5 -102v-7q16 -8 25.5 -26t21.5 -20q21 -3 54.5 8.5t58 10.5t41.5 -30q11 -18 18.5 -38.5t15 -48t12.5 -40.5q17 -46 53 -187q36 -146 57 -197q42 -99 103 -125q43 -12 85 -1.5t76 31.5q131 77 250 237 q104 139 172.5 292.5t82.5 226.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M1292 898q10 216 -161 222q-231 8 -312 -261q44 19 82 19q85 0 74 -96q-4 -57 -74 -167t-105 -110q-43 0 -82 169q-13 54 -45 255q-30 189 -160 177q-59 -7 -164 -100l-81 -72l-81 -72l52 -67q76 52 87 52q57 0 107 -179q15 -55 45 -164.5t45 -164.5q68 -179 164 -179 q157 0 383 294q220 283 226 444zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1152" d="M1152 704q0 -191 -94.5 -353t-256.5 -256.5t-353 -94.5h-160q-14 0 -23 9t-9 23v611l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v93l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v250q0 14 9 23t23 9h160 q14 0 23 -9t9 -23v-181l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-93l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-487q188 13 318 151t130 328q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-352v-352q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v352h-352q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h352v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-352h352q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832 q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="2176" d="M620 416q-110 -64 -268 -64h-128v64h-64q-13 0 -22.5 23.5t-9.5 56.5q0 24 7 49q-58 2 -96.5 10.5t-38.5 20.5t38.5 20.5t96.5 10.5q-7 25 -7 49q0 33 9.5 56.5t22.5 23.5h64v64h128q158 0 268 -64h1113q42 -7 106.5 -18t80.5 -14q89 -15 150 -40.5t83.5 -47.5t22.5 -40 t-22.5 -40t-83.5 -47.5t-150 -40.5q-16 -3 -80.5 -14t-106.5 -18h-1113zM1739 668q53 -36 53 -92t-53 -92l81 -30q68 48 68 122t-68 122zM625 400h1015q-217 -38 -456 -80q-57 0 -113 -24t-83 -48l-28 -24l-288 -288q-26 -26 -70.5 -45t-89.5 -19h-96l-93 464h29 q157 0 273 64zM352 816h-29l93 464h96q46 0 90 -19t70 -45l288 -288q4 -4 11 -10.5t30.5 -23t48.5 -29t61.5 -23t72.5 -10.5l456 -80h-1015q-116 64 -273 64z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1519 760q62 0 103.5 -40.5t41.5 -101.5q0 -97 -93 -130l-172 -59l56 -167q7 -21 7 -47q0 -59 -42 -102t-101 -43q-47 0 -85.5 27t-53.5 72l-55 165l-310 -106l55 -164q8 -24 8 -47q0 -59 -42 -102t-102 -43q-47 0 -85 27t-53 72l-55 163l-153 -53q-29 -9 -50 -9 q-61 0 -101.5 40t-40.5 101q0 47 27.5 85t71.5 53l156 53l-105 313l-156 -54q-26 -8 -48 -8q-60 0 -101 40.5t-41 100.5q0 47 27.5 85t71.5 53l157 53l-53 159q-8 24 -8 47q0 60 42 102.5t102 42.5q47 0 85 -27t53 -72l54 -160l310 105l-54 160q-8 24 -8 47q0 59 42.5 102 t101.5 43q47 0 85.5 -27.5t53.5 -71.5l53 -161l162 55q21 6 43 6q60 0 102.5 -39.5t42.5 -98.5q0 -45 -30 -81.5t-74 -51.5l-157 -54l105 -316l164 56q24 8 46 8zM725 498l310 105l-105 315l-310 -107z" />
|
||||
<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM1280 352v436q-31 -35 -64 -55q-34 -22 -132.5 -85t-151.5 -99q-98 -69 -164 -69v0v0q-66 0 -164 69 q-46 32 -141.5 92.5t-142.5 92.5q-12 8 -33 27t-31 27v-436q0 -40 28 -68t68 -28h832q40 0 68 28t28 68zM1280 925q0 41 -27.5 70t-68.5 29h-832q-40 0 -68 -28t-28 -68q0 -37 30.5 -76.5t67.5 -64.5q47 -32 137.5 -89t129.5 -83q3 -2 17 -11.5t21 -14t21 -13t23.5 -13 t21.5 -9.5t22.5 -7.5t20.5 -2.5t20.5 2.5t22.5 7.5t21.5 9.5t23.5 13t21 13t21 14t17 11.5l267 174q35 23 66.5 62.5t31.5 73.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M127 640q0 163 67 313l367 -1005q-196 95 -315 281t-119 411zM1415 679q0 -19 -2.5 -38.5t-10 -49.5t-11.5 -44t-17.5 -59t-17.5 -58l-76 -256l-278 826q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-75 1 -202 10q-12 1 -20.5 -5t-11.5 -15t-1.5 -18.5t9 -16.5 t19.5 -8l80 -8l120 -328l-168 -504l-280 832q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-7 0 -23 0.5t-26 0.5q105 160 274.5 253.5t367.5 93.5q147 0 280.5 -53t238.5 -149h-10q-55 0 -92 -40.5t-37 -95.5q0 -12 2 -24t4 -21.5t8 -23t9 -21t12 -22.5t12.5 -21 t14.5 -24t14 -23q63 -107 63 -212zM909 573l237 -647q1 -6 5 -11q-126 -44 -255 -44q-112 0 -217 32zM1570 1009q95 -174 95 -369q0 -209 -104 -385.5t-279 -278.5l235 678q59 169 59 276q0 42 -6 79zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286 t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 -215q173 0 331.5 68t273 182.5t182.5 273t68 331.5t-68 331.5t-182.5 273t-273 182.5t-331.5 68t-331.5 -68t-273 -182.5t-182.5 -273t-68 -331.5t68 -331.5t182.5 -273 t273 -182.5t331.5 -68z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1086 1536v-1536l-272 -128q-228 20 -414 102t-293 208.5t-107 272.5q0 140 100.5 263.5t275 205.5t391.5 108v-172q-217 -38 -356.5 -150t-139.5 -255q0 -152 154.5 -267t388.5 -145v1360zM1755 954l37 -390l-525 114l147 83q-119 70 -280 99v172q277 -33 481 -157z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M960 1536l960 -384v-128h-128q0 -26 -20.5 -45t-48.5 -19h-1526q-28 0 -48.5 19t-20.5 45h-128v128zM256 896h256v-768h128v768h256v-768h128v768h256v-768h128v768h256v-768h59q28 0 48.5 -19t20.5 -45v-64h-1664v64q0 26 20.5 45t48.5 19h59v768zM1851 -64 q28 0 48.5 -19t20.5 -45v-128h-1920v128q0 26 20.5 45t48.5 19h1782z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1774 700l18 -316q4 -69 -82 -128t-235 -93.5t-323 -34.5t-323 34.5t-235 93.5t-82 128l18 316l574 -181q22 -7 48 -7t48 7zM2304 1024q0 -23 -22 -31l-1120 -352q-4 -1 -10 -1t-10 1l-652 206q-43 -34 -71 -111.5t-34 -178.5q63 -36 63 -109q0 -69 -58 -107l58 -433 q2 -14 -8 -25q-9 -11 -24 -11h-192q-15 0 -24 11q-10 11 -8 25l58 433q-58 38 -58 107q0 73 65 111q11 207 98 330l-333 104q-22 8 -22 31t22 31l1120 352q4 1 10 1t10 -1l1120 -352q22 -8 22 -31z" />
|
||||
<glyph unicode="" d="M859 579l13 -707q-62 11 -105 11q-41 0 -105 -11l13 707q-40 69 -168.5 295.5t-216.5 374.5t-181 287q58 -15 108 -15q43 0 111 15q63 -111 133.5 -229.5t167 -276.5t138.5 -227q37 61 109.5 177.5t117.5 190t105 176t107 189.5q54 -14 107 -14q56 0 114 14v0 q-28 -39 -60 -88.5t-49.5 -78.5t-56.5 -96t-49 -84q-146 -248 -353 -610z" />
|
||||
<glyph unicode="" d="M768 750h725q12 -67 12 -128q0 -217 -91 -387.5t-259.5 -266.5t-386.5 -96q-157 0 -299 60.5t-245 163.5t-163.5 245t-60.5 299t60.5 299t163.5 245t245 163.5t299 60.5q300 0 515 -201l-209 -201q-123 119 -306 119q-129 0 -238.5 -65t-173.5 -176.5t-64 -243.5 t64 -243.5t173.5 -176.5t238.5 -65q87 0 160 24t120 60t82 82t51.5 87t22.5 78h-436v264z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1095 369q16 -16 0 -31q-62 -62 -199 -62t-199 62q-16 15 0 31q6 6 15 6t15 -6q48 -49 169 -49q120 0 169 49q6 6 15 6t15 -6zM788 550q0 -37 -26 -63t-63 -26t-63.5 26t-26.5 63q0 38 26.5 64t63.5 26t63 -26.5t26 -63.5zM1183 550q0 -37 -26.5 -63t-63.5 -26t-63 26 t-26 63t26 63.5t63 26.5t63.5 -26t26.5 -64zM1434 670q0 49 -35 84t-85 35t-86 -36q-130 90 -311 96l63 283l200 -45q0 -37 26 -63t63 -26t63.5 26.5t26.5 63.5t-26.5 63.5t-63.5 26.5q-54 0 -80 -50l-221 49q-19 5 -25 -16l-69 -312q-180 -7 -309 -97q-35 37 -87 37 q-50 0 -85 -35t-35 -84q0 -35 18.5 -64t49.5 -44q-6 -27 -6 -56q0 -142 140 -243t337 -101q198 0 338 101t140 243q0 32 -7 57q30 15 48 43.5t18 63.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191 t348 71t348 -71t286 -191t191 -286t71 -348z" />
|
||||
<glyph unicode="" d="M939 407q13 -13 0 -26q-53 -53 -171 -53t-171 53q-13 13 0 26q5 6 13 6t13 -6q42 -42 145 -42t145 42q5 6 13 6t13 -6zM676 563q0 -31 -23 -54t-54 -23t-54 23t-23 54q0 32 22.5 54.5t54.5 22.5t54.5 -22.5t22.5 -54.5zM1014 563q0 -31 -23 -54t-54 -23t-54 23t-23 54 q0 32 22.5 54.5t54.5 22.5t54.5 -22.5t22.5 -54.5zM1229 666q0 42 -30 72t-73 30q-42 0 -73 -31q-113 78 -267 82l54 243l171 -39q1 -32 23.5 -54t53.5 -22q32 0 54.5 22.5t22.5 54.5t-22.5 54.5t-54.5 22.5q-48 0 -69 -43l-189 42q-17 5 -21 -13l-60 -268q-154 -6 -265 -83 q-30 32 -74 32q-43 0 -73 -30t-30 -72q0 -30 16 -55t42 -38q-5 -25 -5 -48q0 -122 120 -208.5t289 -86.5q170 0 290 86.5t120 208.5q0 25 -6 49q25 13 40.5 37.5t15.5 54.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M866 697l90 27v62q0 79 -58 135t-138 56t-138 -55.5t-58 -134.5v-283q0 -20 -14 -33.5t-33 -13.5t-32.5 13.5t-13.5 33.5v120h-151v-122q0 -82 57.5 -139t139.5 -57q81 0 138.5 56.5t57.5 136.5v280q0 19 13.5 33t33.5 14q19 0 32.5 -14t13.5 -33v-54zM1199 502v122h-150 v-126q0 -20 -13.5 -33.5t-33.5 -13.5q-19 0 -32.5 14t-13.5 33v123l-90 -26l-60 28v-123q0 -80 58 -137t139 -57t138.5 57t57.5 139zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103 t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1062 824v118q0 42 -30 72t-72 30t-72 -30t-30 -72v-612q0 -175 -126 -299t-303 -124q-178 0 -303.5 125.5t-125.5 303.5v266h328v-262q0 -43 30 -72.5t72 -29.5t72 29.5t30 72.5v620q0 171 126.5 292t301.5 121q176 0 302 -122t126 -294v-136l-195 -58zM1592 602h328 v-266q0 -178 -125.5 -303.5t-303.5 -125.5q-177 0 -303 124.5t-126 300.5v268l131 -61l195 58v-270q0 -42 30 -71.5t72 -29.5t72 29.5t30 71.5v275z" />
|
||||
<glyph unicode="" d="M1472 160v480h-704v704h-480q-93 0 -158.5 -65.5t-65.5 -158.5v-480h704v-704h480q93 0 158.5 65.5t65.5 158.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M328 1254h204v-983h-532v697h328v286zM328 435v369h-123v-369h123zM614 968v-697h205v697h-205zM614 1254v-204h205v204h-205zM901 968h533v-942h-533v163h328v82h-328v697zM1229 435v369h-123v-369h123zM1516 968h532v-942h-532v163h327v82h-327v697zM1843 435v369h-123 v-369h123z" />
|
||||
<glyph unicode="" d="M1046 516q0 -64 -38 -109t-91 -45q-43 0 -70 15v277q28 17 70 17q53 0 91 -45.5t38 -109.5zM703 944q0 -64 -38 -109.5t-91 -45.5q-43 0 -70 15v277q28 17 70 17q53 0 91 -45t38 -109zM1265 513q0 134 -88 229t-213 95q-20 0 -39 -3q-23 -78 -78 -136q-87 -95 -211 -101 v-636l211 41v206q51 -19 117 -19q125 0 213 95t88 229zM922 940q0 134 -88.5 229t-213.5 95q-74 0 -141 -36h-186v-840l211 41v206q55 -19 116 -19q125 0 213.5 95t88.5 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2038" d="M1222 607q75 3 143.5 -20.5t118 -58.5t101 -94.5t84 -108t75.5 -120.5q33 -56 78.5 -109t75.5 -80.5t99 -88.5q-48 -30 -108.5 -57.5t-138.5 -59t-114 -47.5q-44 37 -74 115t-43.5 164.5t-33 180.5t-42.5 168.5t-72.5 123t-122.5 48.5l-10 -2l-6 -4q4 -5 13 -14 q6 -5 28 -23.5t25.5 -22t19 -18t18 -20.5t11.5 -21t10.5 -27.5t4.5 -31t4 -40.5l1 -33q1 -26 -2.5 -57.5t-7.5 -52t-12.5 -58.5t-11.5 -53q-35 1 -101 -9.5t-98 -10.5q-39 0 -72 10q-2 16 -2 47q0 74 3 96q2 13 31.5 41.5t57 59t26.5 51.5q-24 2 -43 -24 q-36 -53 -111.5 -99.5t-136.5 -46.5q-25 0 -75.5 63t-106.5 139.5t-84 96.5q-6 4 -27 30q-482 -112 -513 -112q-16 0 -28 11t-12 27q0 15 8.5 26.5t22.5 14.5l486 106q-8 14 -8 25t5.5 17.5t16 11.5t20 7t23 4.5t18.5 4.5q4 1 15.5 7.5t17.5 6.5q15 0 28 -16t20 -33 q163 37 172 37q17 0 29.5 -11t12.5 -28q0 -15 -8.5 -26t-23.5 -14l-182 -40l-1 -16q-1 -26 81.5 -117.5t104.5 -91.5q47 0 119 80t72 129q0 36 -23.5 53t-51 18.5t-51 11.5t-23.5 34q0 16 10 34l-68 19q43 44 43 117q0 26 -5 58q82 16 144 16q44 0 71.5 -1.5t48.5 -8.5 t31 -13.5t20.5 -24.5t15.5 -33.5t17 -47.5t24 -60l50 25q-3 -40 -23 -60t-42.5 -21t-40 -6.5t-16.5 -20.5zM1282 842q-5 5 -13.5 15.5t-12 14.5t-10.5 11.5t-10 10.5l-8 8t-8.5 7.5t-8 5t-8.5 4.5q-7 3 -14.5 5t-20.5 2.5t-22 0.5h-32.5h-37.5q-126 0 -217 -43 q16 30 36 46.5t54 29.5t65.5 36t46 36.5t50 55t43.5 50.5q12 -9 28 -31.5t32 -36.5t38 -13l12 1v-76l22 -1q247 95 371 190q28 21 50 39t42.5 37.5t33 31t29.5 34t24 31t24.5 37t23 38t27 47.5t29.5 53l7 9q-2 -53 -43 -139q-79 -165 -205 -264t-306 -142q-14 -3 -42 -7.5 t-50 -9.5t-39 -14q3 -19 24.5 -46t21.5 -34q0 -11 -26 -30zM1061 -79q39 26 131.5 47.5t146.5 21.5q9 0 22.5 -15.5t28 -42.5t26 -50t24 -51t14.5 -33q-121 -45 -244 -45q-61 0 -125 11zM822 568l48 12l109 -177l-73 -48zM1323 51q3 -15 3 -16q0 -7 -17.5 -14.5t-46 -13 t-54 -9.5t-53.5 -7.5t-32 -4.5l-7 43q21 2 60.5 8.5t72 10t60.5 3.5h14zM866 679l-96 -20l-6 17q10 1 32.5 7t34.5 6q19 0 35 -10zM1061 45h31l10 -83l-41 -12v95zM1950 1535v1v-1zM1950 1535l-1 -5l-2 -2l1 3zM1950 1535l1 1z" />
|
||||
<glyph unicode="" d="M1167 -50q-5 19 -24 5q-30 -22 -87 -39t-131 -17q-129 0 -193 49q-5 4 -13 4q-11 0 -26 -12q-7 -6 -7.5 -16t7.5 -20q34 -32 87.5 -46t102.5 -12.5t99 4.5q41 4 84.5 20.5t65 30t28.5 20.5q12 12 7 29zM1128 65q-19 47 -39 61q-23 15 -76 15q-47 0 -71 -10 q-29 -12 -78 -56q-26 -24 -12 -44q9 -8 17.5 -4.5t31.5 23.5q3 2 10.5 8.5t10.5 8.5t10 7t11.5 7t12.5 5t15 4.5t16.5 2.5t20.5 1q27 0 44.5 -7.5t23 -14.5t13.5 -22q10 -17 12.5 -20t12.5 1q23 12 14 34zM1483 346q0 22 -5 44.5t-16.5 45t-34 36.5t-52.5 14 q-33 0 -97 -41.5t-129 -83.5t-101 -42q-27 -1 -63.5 19t-76 49t-83.5 58t-100 49t-111 19q-115 -1 -197 -78.5t-84 -178.5q-2 -112 74 -164q29 -20 62.5 -28.5t103.5 -8.5q57 0 132 32.5t134 71t120 70.5t93 31q26 -1 65 -31.5t71.5 -67t68 -67.5t55.5 -32q35 -3 58.5 14 t55.5 63q28 41 42.5 101t14.5 106zM1536 506q0 -164 -62 -304.5t-166 -236t-242.5 -149.5t-290.5 -54t-293 57.5t-247.5 157t-170.5 241.5t-64 302q0 89 19.5 172.5t49 145.5t70.5 118.5t78.5 94t78.5 69.5t64.5 46.5t42.5 24.5q14 8 51 26.5t54.5 28.5t48 30t60.5 44 q36 28 58 72.5t30 125.5q129 -155 186 -193q44 -29 130 -68t129 -66q21 -13 39 -25t60.5 -46.5t76 -70.5t75 -95t69 -122t47 -148.5t19.5 -177.5z" />
|
||||
<glyph unicode="" d="M1070 463l-160 -160l-151 -152l-30 -30q-65 -64 -151.5 -87t-171.5 -2q-16 -70 -72 -115t-129 -45q-85 0 -145 60.5t-60 145.5q0 72 44.5 128t113.5 72q-22 86 1 173t88 152l12 12l151 -152l-11 -11q-37 -37 -37 -89t37 -90q37 -37 89 -37t89 37l30 30l151 152l161 160z M729 1145l12 -12l-152 -152l-12 12q-37 37 -89 37t-89 -37t-37 -89.5t37 -89.5l29 -29l152 -152l160 -160l-151 -152l-161 160l-151 152l-30 30q-68 67 -90 159.5t5 179.5q-70 15 -115 71t-45 129q0 85 60 145.5t145 60.5q76 0 133.5 -49t69.5 -123q84 20 169.5 -3.5 t149.5 -87.5zM1536 78q0 -85 -60 -145.5t-145 -60.5q-74 0 -131 47t-71 118q-86 -28 -179.5 -6t-161.5 90l-11 12l151 152l12 -12q37 -37 89 -37t89 37t37 89t-37 89l-30 30l-152 152l-160 160l152 152l160 -160l152 -152l29 -30q64 -64 87.5 -150.5t2.5 -171.5 q76 -11 126.5 -68.5t50.5 -134.5zM1534 1202q0 -77 -51 -135t-127 -69q26 -85 3 -176.5t-90 -158.5l-12 -12l-151 152l12 12q37 37 37 89t-37 89t-89 37t-89 -37l-30 -30l-152 -152l-160 -160l-152 152l161 160l152 152l29 30q67 67 159 89.5t178 -3.5q11 75 68.5 126 t135.5 51q85 0 145 -60.5t60 -145.5z" />
|
||||
<glyph unicode="" d="M654 458q-1 -3 -12.5 0.5t-31.5 11.5l-20 9q-44 20 -87 49q-7 5 -41 31.5t-38 28.5q-67 -103 -134 -181q-81 -95 -105 -110q-4 -2 -19.5 -4t-18.5 0q6 4 82 92q21 24 85.5 115t78.5 118q17 30 51 98.5t36 77.5q-8 1 -110 -33q-8 -2 -27.5 -7.5t-34.5 -9.5t-17 -5 q-2 -2 -2 -10.5t-1 -9.5q-5 -10 -31 -15q-23 -7 -47 0q-18 4 -28 21q-4 6 -5 23q6 2 24.5 5t29.5 6q58 16 105 32q100 35 102 35q10 2 43 19.5t44 21.5q9 3 21.5 8t14.5 5.5t6 -0.5q2 -12 -1 -33q0 -2 -12.5 -27t-26.5 -53.5t-17 -33.5q-25 -50 -77 -131l64 -28 q12 -6 74.5 -32t67.5 -28q4 -1 10.5 -25.5t4.5 -30.5zM449 944q3 -15 -4 -28q-12 -23 -50 -38q-30 -12 -60 -12q-26 3 -49 26q-14 15 -18 41l1 3q3 -3 19.5 -5t26.5 0t58 16q36 12 55 14q17 0 21 -17zM1147 815l63 -227l-139 42zM39 15l694 232v1032l-694 -233v-1031z M1280 332l102 -31l-181 657l-100 31l-216 -536l102 -31l45 110l211 -65zM777 1294l573 -184v380zM1088 -29l158 -13l-54 -160l-40 66q-130 -83 -276 -108q-58 -12 -91 -12h-84q-79 0 -199.5 39t-183.5 85q-8 7 -8 16q0 8 5 13.5t13 5.5q4 0 18 -7.5t30.5 -16.5t20.5 -11 q73 -37 159.5 -61.5t157.5 -24.5q95 0 167 14.5t157 50.5q15 7 30.5 15.5t34 19t28.5 16.5zM1536 1050v-1079l-774 246q-14 -6 -375 -127.5t-368 -121.5q-13 0 -18 13q0 1 -1 3v1078q3 9 4 10q5 6 20 11q106 35 149 50v384l558 -198q2 0 160.5 55t316 108.5t161.5 53.5 q20 0 20 -21v-418z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M288 1152q66 0 113 -47t47 -113v-1088q0 -66 -47 -113t-113 -47h-128q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h128zM1664 989q58 -34 93 -93t35 -128v-768q0 -106 -75 -181t-181 -75h-864q-66 0 -113 47t-47 113v1536q0 40 28 68t68 28h672q40 0 88 -20t76 -48 l152 -152q28 -28 48 -76t20 -88v-163zM928 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 512v128q0 14 -9 23 t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128 q14 0 23 9t9 23zM1184 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 256v128q0 14 -9 23t-23 9h-128 q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1536 896v256h-160q-40 0 -68 28t-28 68v160h-640v-512h896z" />
|
||||
<glyph unicode="" d="M1344 1536q26 0 45 -19t19 -45v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280zM512 1248v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 992v-64q0 -14 9 -23t23 -9h64q14 0 23 9 t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 736v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 480v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 160v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM384 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 -96v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9 t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM896 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 928v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 160v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9 t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1188 988l-292 -292v-824q0 -46 -33 -79t-79 -33t-79 33t-33 79v384h-64v-384q0 -46 -33 -79t-79 -33t-79 33t-33 79v824l-292 292q-28 28 -28 68t28 68t68 28t68 -28l228 -228h368l228 228q28 28 68 28t68 -28t28 -68t-28 -68zM864 1152q0 -93 -65.5 -158.5 t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M780 1064q0 -60 -19 -113.5t-63 -92.5t-105 -39q-76 0 -138 57.5t-92 135.5t-30 151q0 60 19 113.5t63 92.5t105 39q77 0 138.5 -57.5t91.5 -135t30 -151.5zM438 581q0 -80 -42 -139t-119 -59q-76 0 -141.5 55.5t-100.5 133.5t-35 152q0 80 42 139.5t119 59.5 q76 0 141.5 -55.5t100.5 -134t35 -152.5zM832 608q118 0 255 -97.5t229 -237t92 -254.5q0 -46 -17 -76.5t-48.5 -45t-64.5 -20t-76 -5.5q-68 0 -187.5 45t-182.5 45q-66 0 -192.5 -44.5t-200.5 -44.5q-183 0 -183 146q0 86 56 191.5t139.5 192.5t187.5 146t193 59zM1071 819 q-61 0 -105 39t-63 92.5t-19 113.5q0 74 30 151.5t91.5 135t138.5 57.5q61 0 105 -39t63 -92.5t19 -113.5q0 -73 -30 -151t-92 -135.5t-138 -57.5zM1503 923q77 0 119 -59.5t42 -139.5q0 -74 -35 -152t-100.5 -133.5t-141.5 -55.5q-77 0 -119 59t-42 139q0 74 35 152.5 t100.5 134t141.5 55.5z" />
|
||||
<glyph unicode="" horiz-adv-x="768" d="M704 1008q0 -145 -57 -243.5t-152 -135.5l45 -821q2 -26 -16 -45t-44 -19h-192q-26 0 -44 19t-16 45l45 821q-95 37 -152 135.5t-57 243.5q0 128 42.5 249.5t117.5 200t160 78.5t160 -78.5t117.5 -200t42.5 -249.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M896 -93l640 349v636l-640 -233v-752zM832 772l698 254l-698 254l-698 -254zM1664 1024v-768q0 -35 -18 -65t-49 -47l-704 -384q-28 -16 -61 -16t-61 16l-704 384q-31 17 -49 47t-18 65v768q0 40 23 73t61 47l704 256q22 8 44 8t44 -8l704 -256q38 -14 61 -47t23 -73z " />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M640 -96l384 192v314l-384 -164v-342zM576 358l404 173l-404 173l-404 -173zM1664 -96l384 192v314l-384 -164v-342zM1600 358l404 173l-404 173l-404 -173zM1152 651l384 165v266l-384 -164v-267zM1088 1030l441 189l-441 189l-441 -189zM2176 512v-416q0 -36 -19 -67 t-52 -47l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-5 2 -7 4q-2 -2 -7 -4l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-33 16 -52 47t-19 67v416q0 38 21.5 70t56.5 48l434 186v400q0 38 21.5 70t56.5 48l448 192q23 10 50 10t50 -10l448 -192q35 -16 56.5 -48t21.5 -70 v-400l434 -186q36 -16 57 -48t21 -70z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1848 1197h-511v-124h511v124zM1596 771q-90 0 -146 -52.5t-62 -142.5h408q-18 195 -200 195zM1612 186q63 0 122 32t76 87h221q-100 -307 -427 -307q-214 0 -340.5 132t-126.5 347q0 208 130.5 345.5t336.5 137.5q138 0 240.5 -68t153 -179t50.5 -248q0 -17 -2 -47h-658 q0 -111 57.5 -171.5t166.5 -60.5zM277 236h296q205 0 205 167q0 180 -199 180h-302v-347zM277 773h281q78 0 123.5 36.5t45.5 113.5q0 144 -190 144h-260v-294zM0 1282h594q87 0 155 -14t126.5 -47.5t90 -96.5t31.5 -154q0 -181 -172 -263q114 -32 172 -115t58 -204 q0 -75 -24.5 -136.5t-66 -103.5t-98.5 -71t-121 -42t-134 -13h-611v1260z" />
|
||||
<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM499 1041h-371v-787h382q117 0 197 57.5t80 170.5q0 158 -143 200q107 52 107 164q0 57 -19.5 96.5 t-56.5 60.5t-79 29.5t-97 8.5zM477 723h-176v184h163q119 0 119 -90q0 -94 -106 -94zM486 388h-185v217h189q124 0 124 -113q0 -104 -128 -104zM1136 356q-68 0 -104 38t-36 107h411q1 10 1 30q0 132 -74.5 220.5t-203.5 88.5q-128 0 -210 -86t-82 -216q0 -135 79 -217 t213 -82q205 0 267 191h-138q-11 -34 -47.5 -54t-75.5 -20zM1126 722q113 0 124 -122h-254q4 56 39 89t91 33zM964 988h319v-77h-319v77z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1582 954q0 -101 -71.5 -172.5t-172.5 -71.5t-172.5 71.5t-71.5 172.5t71.5 172.5t172.5 71.5t172.5 -71.5t71.5 -172.5zM812 212q0 104 -73 177t-177 73q-27 0 -54 -6l104 -42q77 -31 109.5 -106.5t1.5 -151.5q-31 -77 -107 -109t-152 -1q-21 8 -62 24.5t-61 24.5 q32 -60 91 -96.5t130 -36.5q104 0 177 73t73 177zM1642 953q0 126 -89.5 215.5t-215.5 89.5q-127 0 -216.5 -89.5t-89.5 -215.5q0 -127 89.5 -216t216.5 -89q126 0 215.5 89t89.5 216zM1792 953q0 -189 -133.5 -322t-321.5 -133l-437 -319q-12 -129 -109 -218t-229 -89 q-121 0 -214 76t-118 192l-230 92v429l389 -157q79 48 173 48q13 0 35 -2l284 407q2 187 135.5 319t320.5 132q188 0 321.5 -133.5t133.5 -321.5z" />
|
||||
<glyph unicode="" d="M1242 889q0 80 -57 136.5t-137 56.5t-136.5 -57t-56.5 -136q0 -80 56.5 -136.5t136.5 -56.5t137 56.5t57 136.5zM632 301q0 -83 -58 -140.5t-140 -57.5q-56 0 -103 29t-72 77q52 -20 98 -40q60 -24 120 1.5t85 86.5q24 60 -1.5 120t-86.5 84l-82 33q22 5 42 5 q82 0 140 -57.5t58 -140.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v153l172 -69q20 -92 93.5 -152t168.5 -60q104 0 181 70t87 173l345 252q150 0 255.5 105.5t105.5 254.5q0 150 -105.5 255.5t-255.5 105.5 q-148 0 -253 -104.5t-107 -252.5l-225 -322q-9 1 -28 1q-75 0 -137 -37l-297 119v468q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5zM1289 887q0 -100 -71 -170.5t-171 -70.5t-170.5 70.5t-70.5 170.5t70.5 171t170.5 71q101 0 171.5 -70.5t70.5 -171.5z " />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M836 367l-15 -368l-2 -22l-420 29q-36 3 -67 31.5t-47 65.5q-11 27 -14.5 55t4 65t12 55t21.5 64t19 53q78 -12 509 -28zM449 953l180 -379l-147 92q-63 -72 -111.5 -144.5t-72.5 -125t-39.5 -94.5t-18.5 -63l-4 -21l-190 357q-17 26 -18 56t6 47l8 18q35 63 114 188 l-140 86zM1680 436l-188 -359q-12 -29 -36.5 -46.5t-43.5 -20.5l-18 -4q-71 -7 -219 -12l8 -164l-230 367l211 362l7 -173q170 -16 283 -5t170 33zM895 1360q-47 -63 -265 -435l-317 187l-19 12l225 356q20 31 60 45t80 10q24 -2 48.5 -12t42 -21t41.5 -33t36 -34.5 t36 -39.5t32 -35zM1550 1053l212 -363q18 -37 12.5 -76t-27.5 -74q-13 -20 -33 -37t-38 -28t-48.5 -22t-47 -16t-51.5 -14t-46 -12q-34 72 -265 436l313 195zM1407 1279l142 83l-220 -373l-419 20l151 86q-34 89 -75 166t-75.5 123.5t-64.5 80t-47 46.5l-17 13l405 -1 q31 3 58 -10.5t39 -28.5l11 -15q39 -61 112 -190z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M480 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM516 768h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5zM1888 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM2048 544v-384 q0 -14 -9 -23t-23 -9h-96v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-1024v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5t179 63.5h768q98 0 179 -63.5t104 -157.5 l105 -419h28q93 0 158.5 -65.5t65.5 -158.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1824 640q93 0 158.5 -65.5t65.5 -158.5v-384q0 -14 -9 -23t-23 -9h-96v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-1024v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5 t179 63.5h128v224q0 14 9 23t23 9h448q14 0 23 -9t9 -23v-224h128q98 0 179 -63.5t104 -157.5l105 -419h28zM320 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM516 640h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5z M1728 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47z" />
|
||||
<glyph unicode="" d="M1504 64q0 -26 -19 -45t-45 -19h-462q1 -17 6 -87.5t5 -108.5q0 -25 -18 -42.5t-43 -17.5h-320q-25 0 -43 17.5t-18 42.5q0 38 5 108.5t6 87.5h-462q-26 0 -45 19t-19 45t19 45l402 403h-229q-26 0 -45 19t-19 45t19 45l402 403h-197q-26 0 -45 19t-19 45t19 45l384 384 q19 19 45 19t45 -19l384 -384q19 -19 19 -45t-19 -45t-45 -19h-197l402 -403q19 -19 19 -45t-19 -45t-45 -19h-229l402 -403q19 -19 19 -45z" />
|
||||
<glyph unicode="" d="M1127 326q0 32 -30 51q-193 115 -447 115q-133 0 -287 -34q-42 -9 -42 -52q0 -20 13.5 -34.5t35.5 -14.5q5 0 37 8q132 27 243 27q226 0 397 -103q19 -11 33 -11q19 0 33 13.5t14 34.5zM1223 541q0 40 -35 61q-237 141 -548 141q-153 0 -303 -42q-48 -13 -48 -64 q0 -25 17.5 -42.5t42.5 -17.5q7 0 37 8q122 33 251 33q279 0 488 -124q24 -13 38 -13q25 0 42.5 17.5t17.5 42.5zM1331 789q0 47 -40 70q-126 73 -293 110.5t-343 37.5q-204 0 -364 -47q-23 -7 -38.5 -25.5t-15.5 -48.5q0 -31 20.5 -52t51.5 -21q11 0 40 8q133 37 307 37 q159 0 309.5 -34t253.5 -95q21 -12 40 -12q29 0 50.5 20.5t21.5 51.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M1024 1233l-303 -582l24 -31h279v-415h-507l-44 -30l-142 -273l-30 -30h-301v303l303 583l-24 30h-279v415h507l44 30l142 273l30 30h301v-303z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M784 164l16 241l-16 523q-1 10 -7.5 17t-16.5 7q-9 0 -16 -7t-7 -17l-14 -523l14 -241q1 -10 7.5 -16.5t15.5 -6.5q22 0 24 23zM1080 193l11 211l-12 586q0 16 -13 24q-8 5 -16 5t-16 -5q-13 -8 -13 -24l-1 -6l-10 -579q0 -1 11 -236v-1q0 -10 6 -17q9 -11 23 -11 q11 0 20 9q9 7 9 20zM35 533l20 -128l-20 -126q-2 -9 -9 -9t-9 9l-17 126l17 128q2 9 9 9t9 -9zM121 612l26 -207l-26 -203q-2 -9 -10 -9q-9 0 -9 10l-23 202l23 207q0 9 9 9q8 0 10 -9zM401 159zM213 650l25 -245l-25 -237q0 -11 -11 -11q-10 0 -12 11l-21 237l21 245 q2 12 12 12q11 0 11 -12zM307 657l23 -252l-23 -244q-2 -13 -14 -13q-13 0 -13 13l-21 244l21 252q0 13 13 13q12 0 14 -13zM401 639l21 -234l-21 -246q-2 -16 -16 -16q-6 0 -10.5 4.5t-4.5 11.5l-20 246l20 234q0 6 4.5 10.5t10.5 4.5q14 0 16 -15zM784 164zM495 785 l21 -380l-21 -246q0 -7 -5 -12.5t-12 -5.5q-16 0 -18 18l-18 246l18 380q2 18 18 18q7 0 12 -5.5t5 -12.5zM589 871l19 -468l-19 -244q0 -8 -5.5 -13.5t-13.5 -5.5q-18 0 -20 19l-16 244l16 468q2 19 20 19q8 0 13.5 -5.5t5.5 -13.5zM687 911l18 -506l-18 -242 q-2 -21 -22 -21q-19 0 -21 21l-16 242l16 506q0 9 6.5 15.5t14.5 6.5q9 0 15 -6.5t7 -15.5zM1079 169v0v0zM881 915l15 -510l-15 -239q0 -10 -7.5 -17.5t-17.5 -7.5t-17 7t-8 18l-14 239l14 510q0 11 7.5 18t17.5 7t17.5 -7t7.5 -18zM980 896l14 -492l-14 -236q0 -11 -8 -19 t-19 -8t-19 8t-9 19l-12 236l12 492q1 12 9 20t19 8t18.5 -8t8.5 -20zM1192 404l-14 -231v0q0 -13 -9 -22t-22 -9t-22 9t-10 22l-6 114l-6 117l12 636v3q2 15 12 24q9 7 20 7q8 0 15 -5q14 -8 16 -26zM2304 423q0 -117 -83 -199.5t-200 -82.5h-786q-13 2 -22 11t-9 22v899 q0 23 28 33q85 34 181 34q195 0 338 -131.5t160 -323.5q53 22 110 22q117 0 200 -83t83 -201z" />
|
||||
<glyph unicode="" d="M768 768q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 0q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127 t443 -43zM768 384q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 1536q208 0 385 -34.5t280 -93.5t103 -128v-128q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5 t-103 128v128q0 69 103 128t280 93.5t385 34.5z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M894 465q33 -26 84 -56q59 7 117 7q147 0 177 -49q16 -22 2 -52q0 -1 -1 -2l-2 -2v-1q-6 -38 -71 -38q-48 0 -115 20t-130 53q-221 -24 -392 -83q-153 -262 -242 -262q-15 0 -28 7l-24 12q-1 1 -6 5q-10 10 -6 36q9 40 56 91.5t132 96.5q14 9 23 -6q2 -2 2 -4q52 85 107 197 q68 136 104 262q-24 82 -30.5 159.5t6.5 127.5q11 40 42 40h21h1q23 0 35 -15q18 -21 9 -68q-2 -6 -4 -8q1 -3 1 -8v-30q-2 -123 -14 -192q55 -164 146 -238zM318 54q52 24 137 158q-51 -40 -87.5 -84t-49.5 -74zM716 974q-15 -42 -2 -132q1 7 7 44q0 3 7 43q1 4 4 8 q-1 1 -1 2t-0.5 1.5t-0.5 1.5q-1 22 -13 36q0 -1 -1 -2v-2zM592 313q135 54 284 81q-2 1 -13 9.5t-16 13.5q-76 67 -127 176q-27 -86 -83 -197q-30 -56 -45 -83zM1238 329q-24 24 -140 24q76 -28 124 -28q14 0 18 1q0 1 -2 3z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M233 768v-107h70l164 -661h159l128 485q7 20 10 46q2 16 2 24h4l3 -24q1 -3 3.5 -20t5.5 -26l128 -485h159l164 661h70v107h-300v-107h90l-99 -438q-5 -20 -7 -46l-2 -21h-4l-3 21q-1 5 -4 21t-5 25l-144 545h-114l-144 -545q-2 -9 -4.5 -24.5t-3.5 -21.5l-4 -21h-4l-2 21 q-2 26 -7 46l-99 438h90v107h-300z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M429 106v-106h281v106h-75l103 161q5 7 10 16.5t7.5 13.5t3.5 4h2q1 -4 5 -10q2 -4 4.5 -7.5t6 -8t6.5 -8.5l107 -161h-76v-106h291v106h-68l-192 273l195 282h67v107h-279v-107h74l-103 -159q-4 -7 -10 -16.5t-9 -13.5l-2 -3h-2q-1 4 -5 10q-6 11 -17 23l-106 159h76v107 h-290v-107h68l189 -272l-194 -283h-68z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M416 106v-106h327v106h-93v167h137q76 0 118 15q67 23 106.5 87t39.5 146q0 81 -37 141t-100 87q-48 19 -130 19h-368v-107h92v-555h-92zM769 386h-119v268h120q52 0 83 -18q56 -33 56 -115q0 -89 -62 -120q-31 -15 -78 -15z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M1280 320v-320h-1024v192l192 192l128 -128l384 384zM448 512q-80 0 -136 56t-56 136t56 136t136 56t136 -56t56 -136t-56 -136t-136 -56z" />
|
||||
<glyph unicode="" d="M640 1152v128h-128v-128h128zM768 1024v128h-128v-128h128zM640 896v128h-128v-128h128zM768 768v128h-128v-128h128zM1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400 v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-128v-128h-128v128h-512v-1536h1280zM781 593l107 -349q8 -27 8 -52q0 -83 -72.5 -137.5t-183.5 -54.5t-183.5 54.5t-72.5 137.5q0 25 8 52q21 63 120 396v128h128v-128h79 q22 0 39 -13t23 -34zM640 128q53 0 90.5 19t37.5 45t-37.5 45t-90.5 19t-90.5 -19t-37.5 -45t37.5 -45t90.5 -19z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M620 686q20 -8 20 -30v-544q0 -22 -20 -30q-8 -2 -12 -2q-12 0 -23 9l-166 167h-131q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h131l166 167q16 15 35 7zM1037 -3q31 0 50 24q129 159 129 363t-129 363q-16 21 -43 24t-47 -14q-21 -17 -23.5 -43.5t14.5 -47.5 q100 -123 100 -282t-100 -282q-17 -21 -14.5 -47.5t23.5 -42.5q18 -15 40 -15zM826 145q27 0 47 20q87 93 87 219t-87 219q-18 19 -45 20t-46 -17t-20 -44.5t18 -46.5q52 -57 52 -131t-52 -131q-19 -20 -18 -46.5t20 -44.5q20 -17 44 -17z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M768 768q52 0 90 -38t38 -90v-384q0 -52 -38 -90t-90 -38h-384q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h384zM1260 766q20 -8 20 -30v-576q0 -22 -20 -30q-8 -2 -12 -2q-14 0 -23 9l-265 266v90l265 266q9 9 23 9q4 0 12 -2z" />
|
||||
<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M480 768q8 11 21 12.5t24 -6.5l51 -38q11 -8 12.5 -21t-6.5 -24l-182 -243l182 -243q8 -11 6.5 -24t-12.5 -21l-51 -38q-11 -8 -24 -6.5t-21 12.5l-226 301q-14 19 0 38zM1282 467q14 -19 0 -38l-226 -301q-8 -11 -21 -12.5t-24 6.5l-51 38q-11 8 -12.5 21t6.5 24l182 243 l-182 243q-8 11 -6.5 24t12.5 21l51 38q11 8 24 6.5t21 -12.5zM662 6q-13 2 -20.5 13t-5.5 24l138 831q2 13 13 20.5t24 5.5l63 -10q13 -2 20.5 -13t5.5 -24l-138 -831q-2 -13 -13 -20.5t-24 -5.5z" />
|
||||
<glyph unicode="" d="M1497 709v-198q-101 -23 -198 -23q-65 -136 -165.5 -271t-181.5 -215.5t-128 -106.5q-80 -45 -162 3q-28 17 -60.5 43.5t-85 83.5t-102.5 128.5t-107.5 184t-105.5 244t-91.5 314.5t-70.5 390h283q26 -218 70 -398.5t104.5 -317t121.5 -235.5t140 -195q169 169 287 406 q-142 72 -223 220t-81 333q0 192 104 314.5t284 122.5q178 0 273 -105.5t95 -297.5q0 -159 -58 -286q-7 -1 -19.5 -3t-46 -2t-63 6t-62 25.5t-50.5 51.5q31 103 31 184q0 87 -29 132t-79 45q-53 0 -85 -49.5t-32 -140.5q0 -186 105 -293.5t267 -107.5q62 0 121 14z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M216 367l603 -402v359l-334 223zM154 511l193 129l-193 129v-258zM973 -35l603 402l-269 180l-334 -223v-359zM896 458l272 182l-272 182l-272 -182zM485 733l334 223v359l-603 -402zM1445 640l193 -129v258zM1307 733l269 180l-603 402v-359zM1792 913v-546 q0 -41 -34 -64l-819 -546q-21 -13 -43 -13t-43 13l-819 546q-34 23 -34 64v546q0 41 34 64l819 546q21 13 43 13t43 -13l819 -546q34 -23 34 -64z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1800 764q111 -46 179.5 -145.5t68.5 -221.5q0 -164 -118 -280.5t-285 -116.5q-4 0 -11.5 0.5t-10.5 0.5h-1209h-1h-2h-5q-170 10 -288 125.5t-118 280.5q0 110 55 203t147 147q-12 39 -12 82q0 115 82 196t199 81q95 0 172 -58q75 154 222.5 248t326.5 94 q166 0 306 -80.5t221.5 -218.5t81.5 -301q0 -6 -0.5 -18t-0.5 -18zM468 498q0 -122 84 -193t208 -71q137 0 240 99q-16 20 -47.5 56.5t-43.5 50.5q-67 -65 -144 -65q-55 0 -93.5 33.5t-38.5 87.5q0 53 38.5 87t91.5 34q44 0 84.5 -21t73 -55t65 -75t69 -82t77 -75t97 -55 t121.5 -21q121 0 204.5 71.5t83.5 190.5q0 121 -84 192t-207 71q-143 0 -241 -97q14 -16 29.5 -34t34.5 -40t29 -34q66 64 142 64q52 0 92 -33t40 -84q0 -57 -37 -91.5t-94 -34.5q-43 0 -82.5 21t-72 55t-65.5 75t-69.5 82t-77.5 75t-96.5 55t-118.5 21q-122 0 -207 -70.5 t-85 -189.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 1408q-190 0 -361 -90l194 -194q82 28 167 28t167 -28l194 194q-171 90 -361 90zM218 279l194 194 q-28 82 -28 167t28 167l-194 194q-90 -171 -90 -361t90 -361zM896 -128q190 0 361 90l-194 194q-82 -28 -167 -28t-167 28l-194 -194q171 -90 361 -90zM896 256q159 0 271.5 112.5t112.5 271.5t-112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5 t271.5 -112.5zM1380 473l194 -194q90 171 90 361t-90 361l-194 -194q28 -82 28 -167t-28 -167z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1760 640q0 -176 -68.5 -336t-184 -275.5t-275.5 -184t-336 -68.5t-336 68.5t-275.5 184t-184 275.5t-68.5 336q0 213 97 398.5t265 305.5t374 151v-228q-221 -45 -366.5 -221t-145.5 -406q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5 t136.5 204t51 248.5q0 230 -145.5 406t-366.5 221v228q206 -31 374 -151t265 -305.5t97 -398.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M19 662q8 217 116 406t305 318h5q0 -1 -1 -3q-8 -8 -28 -33.5t-52 -76.5t-60 -110.5t-44.5 -135.5t-14 -150.5t39 -157.5t108.5 -154q50 -50 102 -69.5t90.5 -11.5t69.5 23.5t47 32.5l16 16q39 51 53 116.5t6.5 122.5t-21 107t-26.5 80l-14 29q-10 25 -30.5 49.5t-43 41 t-43.5 29.5t-35 19l-13 6l104 115q39 -17 78 -52t59 -61l19 -27q1 48 -18.5 103.5t-40.5 87.5l-20 31l161 183l160 -181q-33 -46 -52.5 -102.5t-22.5 -90.5l-4 -33q22 37 61.5 72.5t67.5 52.5l28 17l103 -115q-44 -14 -85 -50t-60 -65l-19 -29q-31 -56 -48 -133.5t-7 -170 t57 -156.5q33 -45 77.5 -60.5t85 -5.5t76 26.5t57.5 33.5l21 16q60 53 96.5 115t48.5 121.5t10 121.5t-18 118t-37 107.5t-45.5 93t-45 72t-34.5 47.5l-13 17q-14 13 -7 13l10 -3q40 -29 62.5 -46t62 -50t64 -58t58.5 -65t55.5 -77t45.5 -88t38 -103t23.5 -117t10.5 -136 q3 -259 -108 -465t-312 -321t-456 -115q-185 0 -351 74t-283.5 198t-184 293t-60.5 353z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M874 -102v-66q-208 6 -385 109.5t-283 275.5l58 34q29 -49 73 -99l65 57q148 -168 368 -212l-17 -86q65 -12 121 -13zM276 428l-83 -28q22 -60 49 -112l-57 -33q-98 180 -98 385t98 385l57 -33q-30 -56 -49 -112l82 -28q-35 -100 -35 -212q0 -109 36 -212zM1528 251 l58 -34q-106 -172 -283 -275.5t-385 -109.5v66q56 1 121 13l-17 86q220 44 368 212l65 -57q44 50 73 99zM1377 805l-233 -80q14 -42 14 -85t-14 -85l232 -80q-31 -92 -98 -169l-185 162q-57 -67 -147 -85l48 -241q-52 -10 -98 -10t-98 10l48 241q-90 18 -147 85l-185 -162 q-67 77 -98 169l232 80q-14 42 -14 85t14 85l-233 80q33 93 99 169l185 -162q59 68 147 86l-48 240q44 10 98 10t98 -10l-48 -240q88 -18 147 -86l185 162q66 -76 99 -169zM874 1448v-66q-65 -2 -121 -13l17 -86q-220 -42 -368 -211l-65 56q-38 -42 -73 -98l-57 33 q106 172 282 275.5t385 109.5zM1705 640q0 -205 -98 -385l-57 33q27 52 49 112l-83 28q36 103 36 212q0 112 -35 212l82 28q-19 56 -49 112l57 33q98 -180 98 -385zM1585 1063l-57 -33q-35 56 -73 98l-65 -56q-148 169 -368 211l17 86q-56 11 -121 13v66q209 -6 385 -109.5 t282 -275.5zM1748 640q0 173 -67.5 331t-181.5 272t-272 181.5t-331 67.5t-331 -67.5t-272 -181.5t-181.5 -272t-67.5 -331t67.5 -331t181.5 -272t272 -181.5t331 -67.5t331 67.5t272 181.5t181.5 272t67.5 331zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71 t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
|
||||
<glyph unicode="" d="M582 228q0 -66 -93 -66q-107 0 -107 63q0 64 98 64q102 0 102 -61zM546 694q0 -85 -74 -85q-77 0 -77 84q0 90 77 90q36 0 55 -25.5t19 -63.5zM712 769v125q-78 -29 -135 -29q-50 29 -110 29q-86 0 -145 -57t-59 -143q0 -50 29.5 -102t73.5 -67v-3q-38 -17 -38 -85 q0 -53 41 -77v-3q-113 -37 -113 -139q0 -45 20 -78.5t54 -51t72 -25.5t81 -8q224 0 224 188q0 67 -48 99t-126 46q-27 5 -51.5 20.5t-24.5 39.5q0 44 49 52q77 15 122 70t45 134q0 24 -10 52q37 9 49 13zM771 350h137q-2 27 -2 82v387q0 46 2 69h-137q3 -23 3 -71v-392 q0 -50 -3 -75zM1280 366v121q-30 -21 -68 -21q-53 0 -53 82v225h52q9 0 26.5 -1t26.5 -1v117h-105q0 82 3 102h-140q4 -24 4 -55v-47h-60v-117q36 3 37 3q3 0 11 -0.5t12 -0.5v-2h-2v-217q0 -37 2.5 -64t11.5 -56.5t24.5 -48.5t43.5 -31t66 -12q64 0 108 24zM924 1072 q0 36 -24 63.5t-60 27.5t-60.5 -27t-24.5 -64q0 -36 25 -62.5t60 -26.5t59.5 27t24.5 62zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M595 22q0 100 -165 100q-158 0 -158 -104q0 -101 172 -101q151 0 151 105zM536 777q0 61 -30 102t-89 41q-124 0 -124 -145q0 -135 124 -135q119 0 119 137zM805 1101v-202q-36 -12 -79 -22q16 -43 16 -84q0 -127 -73 -216.5t-197 -112.5q-40 -8 -59.5 -27t-19.5 -58 q0 -31 22.5 -51.5t58 -32t78.5 -22t86 -25.5t78.5 -37.5t58 -64t22.5 -98.5q0 -304 -363 -304q-69 0 -130 12.5t-116 41t-87.5 82t-32.5 127.5q0 165 182 225v4q-67 41 -67 126q0 109 63 137v4q-72 24 -119.5 108.5t-47.5 165.5q0 139 95 231.5t235 92.5q96 0 178 -47 q98 0 218 47zM1123 220h-222q4 45 4 134v609q0 94 -4 128h222q-4 -33 -4 -124v-613q0 -89 4 -134zM1724 442v-196q-71 -39 -174 -39q-62 0 -107 20t-70 50t-39.5 78t-18.5 92t-4 103v351h2v4q-7 0 -19 1t-18 1q-21 0 -59 -6v190h96v76q0 54 -6 89h227q-6 -41 -6 -165h171 v-190q-15 0 -43.5 2t-42.5 2h-85v-365q0 -131 87 -131q61 0 109 33zM1148 1389q0 -58 -39 -101.5t-96 -43.5q-58 0 -98 43.5t-40 101.5q0 59 39.5 103t98.5 44q58 0 96.5 -44.5t38.5 -102.5z" />
|
||||
<glyph unicode="" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M842 964q0 -80 -57 -136.5t-136 -56.5q-60 0 -111 35q-62 -67 -115 -146q-247 -371 -202 -859q1 -22 -12.5 -38.5t-34.5 -18.5h-5q-20 0 -35 13.5t-17 33.5q-14 126 -3.5 247.5t29.5 217t54 186t69 155.5t74 125q61 90 132 165q-16 35 -16 77q0 80 56.5 136.5t136.5 56.5 t136.5 -56.5t56.5 -136.5zM1223 953q0 -158 -78 -292t-212.5 -212t-292.5 -78q-64 0 -131 14q-21 5 -32.5 23.5t-6.5 39.5q5 20 23 31.5t39 7.5q51 -13 108 -13q97 0 186 38t153 102t102 153t38 186t-38 186t-102 153t-153 102t-186 38t-186 -38t-153 -102t-102 -153 t-38 -186q0 -114 52 -218q10 -20 3.5 -40t-25.5 -30t-39.5 -3t-30.5 26q-64 123 -64 265q0 119 46.5 227t124.5 186t186 124t226 46q158 0 292.5 -78t212.5 -212.5t78 -292.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M270 730q-8 19 -8 52q0 20 11 49t24 45q-1 22 7.5 53t22.5 43q0 139 92.5 288.5t217.5 209.5q139 66 324 66q133 0 266 -55q49 -21 90 -48t71 -56t55 -68t42 -74t32.5 -84.5t25.5 -89.5t22 -98l1 -5q55 -83 55 -150q0 -14 -9 -40t-9 -38q0 -1 1.5 -3.5t3.5 -5t2 -3.5 q77 -114 120.5 -214.5t43.5 -208.5q0 -43 -19.5 -100t-55.5 -57q-9 0 -19.5 7.5t-19 17.5t-19 26t-16 26.5t-13.5 26t-9 17.5q-1 1 -3 1l-5 -4q-59 -154 -132 -223q20 -20 61.5 -38.5t69 -41.5t35.5 -65q-2 -4 -4 -16t-7 -18q-64 -97 -302 -97q-53 0 -110.5 9t-98 20 t-104.5 30q-15 5 -23 7q-14 4 -46 4.5t-40 1.5q-41 -45 -127.5 -65t-168.5 -20q-35 0 -69 1.5t-93 9t-101 20.5t-74.5 40t-32.5 64q0 40 10 59.5t41 48.5q11 2 40.5 13t49.5 12q4 0 14 2q2 2 2 4l-2 3q-48 11 -108 105.5t-73 156.5l-5 3q-4 0 -12 -20q-18 -41 -54.5 -74.5 t-77.5 -37.5h-1q-4 0 -6 4.5t-5 5.5q-23 54 -23 100q0 275 252 466z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M580 1075q0 41 -25 66t-66 25q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 66 24.5t25 65.5zM1323 568q0 28 -25.5 50t-65.5 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q40 0 65.5 22t25.5 51zM1087 1075q0 41 -24.5 66t-65.5 25 q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 65.5 24.5t24.5 65.5zM1722 568q0 28 -26 50t-65 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q39 0 65 22t26 51zM1456 965q-31 4 -70 4q-169 0 -311 -77t-223.5 -208.5t-81.5 -287.5 q0 -78 23 -152q-35 -3 -68 -3q-26 0 -50 1.5t-55 6.5t-44.5 7t-54.5 10.5t-50 10.5l-253 -127l72 218q-290 203 -290 490q0 169 97.5 311t264 223.5t363.5 81.5q176 0 332.5 -66t262 -182.5t136.5 -260.5zM2048 404q0 -117 -68.5 -223.5t-185.5 -193.5l55 -181l-199 109 q-150 -37 -218 -37q-169 0 -311 70.5t-223.5 191.5t-81.5 264t81.5 264t223.5 191.5t311 70.5q161 0 303 -70.5t227.5 -192t85.5 -263.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-453 185l-242 -295q-18 -23 -49 -23q-13 0 -22 4q-19 7 -30.5 23.5t-11.5 36.5v349l864 1059l-1069 -925l-395 162q-37 14 -40 55q-2 40 32 59l1664 960q15 9 32 9q20 0 36 -11z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-527 215l-298 -327q-18 -21 -47 -21q-14 0 -23 4q-19 7 -30 23.5t-11 36.5v452l-472 193q-37 14 -40 55q-3 39 32 59l1664 960q35 21 68 -2zM1422 26l221 1323l-1434 -827l336 -137 l863 639l-478 -797z" />
|
||||
<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298zM896 928v-448q0 -14 -9 -23 t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" d="M768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1682 -128q-44 0 -132.5 3.5t-133.5 3.5q-44 0 -132 -3.5t-132 -3.5q-24 0 -37 20.5t-13 45.5q0 31 17 46t39 17t51 7t45 15q33 21 33 140l-1 391q0 21 -1 31q-13 4 -50 4h-675q-38 0 -51 -4q-1 -10 -1 -31l-1 -371q0 -142 37 -164q16 -10 48 -13t57 -3.5t45 -15 t20 -45.5q0 -26 -12.5 -48t-36.5 -22q-47 0 -139.5 3.5t-138.5 3.5q-43 0 -128 -3.5t-127 -3.5q-23 0 -35.5 21t-12.5 45q0 30 15.5 45t36 17.5t47.5 7.5t42 15q33 23 33 143l-1 57v813q0 3 0.5 26t0 36.5t-1.5 38.5t-3.5 42t-6.5 36.5t-11 31.5t-16 18q-15 10 -45 12t-53 2 t-41 14t-18 45q0 26 12 48t36 22q46 0 138.5 -3.5t138.5 -3.5q42 0 126.5 3.5t126.5 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17 -43.5t-38.5 -14.5t-49.5 -4t-43 -13q-35 -21 -35 -160l1 -320q0 -21 1 -32q13 -3 39 -3h699q25 0 38 3q1 11 1 32l1 320q0 139 -35 160 q-18 11 -58.5 12.5t-66 13t-25.5 49.5q0 26 12.5 48t37.5 22q44 0 132 -3.5t132 -3.5q43 0 129 3.5t129 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17.5 -44t-40 -14.5t-51.5 -3t-44 -12.5q-35 -23 -35 -161l1 -943q0 -119 34 -140q16 -10 46 -13.5t53.5 -4.5t41.5 -15.5t18 -44.5 q0 -26 -12 -48t-36 -22z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1278 1347v-73q0 -29 -18.5 -61t-42.5 -32q-50 0 -54 -1q-26 -6 -32 -31q-3 -11 -3 -64v-1152q0 -25 -18 -43t-43 -18h-108q-25 0 -43 18t-18 43v1218h-143v-1218q0 -25 -17.5 -43t-43.5 -18h-108q-26 0 -43.5 18t-17.5 43v496q-147 12 -245 59q-126 58 -192 179 q-64 117 -64 259q0 166 88 286q88 118 209 159q111 37 417 37h479q25 0 43 -18t18 -43z" />
|
||||
<glyph unicode="" d="M352 128v-128h-352v128h352zM704 256q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM864 640v-128h-864v128h864zM224 1152v-128h-224v128h224zM1536 128v-128h-736v128h736zM576 1280q26 0 45 -19t19 -45v-256 q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1216 768q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1536 640v-128h-224v128h224zM1536 1152v-128h-864v128h864z" />
|
||||
<glyph unicode="" d="M1216 512q133 0 226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5t-226.5 93.5t-93.5 226.5q0 12 2 34l-360 180q-92 -86 -218 -86q-133 0 -226.5 93.5t-93.5 226.5t93.5 226.5t226.5 93.5q126 0 218 -86l360 180q-2 22 -2 34q0 133 93.5 226.5t226.5 93.5 t226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5q-126 0 -218 86l-360 -180q2 -22 2 -34t-2 -34l360 -180q92 86 218 86z" />
|
||||
<glyph unicode="" d="M1280 341q0 88 -62.5 151t-150.5 63q-84 0 -145 -58l-241 120q2 16 2 23t-2 23l241 120q61 -58 145 -58q88 0 150.5 63t62.5 151t-62.5 150.5t-150.5 62.5t-151 -62.5t-63 -150.5q0 -7 2 -23l-241 -120q-62 57 -145 57q-88 0 -150.5 -62.5t-62.5 -150.5t62.5 -150.5 t150.5 -62.5q83 0 145 57l241 -120q-2 -16 -2 -23q0 -88 63 -150.5t151 -62.5t150.5 62.5t62.5 150.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M571 947q-10 25 -34 35t-49 0q-108 -44 -191 -127t-127 -191q-10 -25 0 -49t35 -34q13 -5 24 -5q42 0 60 40q34 84 98.5 148.5t148.5 98.5q25 11 35 35t0 49zM1513 1303l46 -46l-244 -243l68 -68q19 -19 19 -45.5t-19 -45.5l-64 -64q89 -161 89 -343q0 -143 -55.5 -273.5 t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5q182 0 343 -89l64 64q19 19 45.5 19t45.5 -19l68 -68zM1521 1359q-10 -10 -22 -10q-13 0 -23 10l-91 90q-9 10 -9 23t9 23q10 9 23 9t23 -9l90 -91 q10 -9 10 -22.5t-10 -22.5zM1751 1129q-11 -9 -23 -9t-23 9l-90 91q-10 9 -10 22.5t10 22.5q9 10 22.5 10t22.5 -10l91 -90q9 -10 9 -23t-9 -23zM1792 1312q0 -14 -9 -23t-23 -9h-96q-14 0 -23 9t-9 23t9 23t23 9h96q14 0 23 -9t9 -23zM1600 1504v-96q0 -14 -9 -23t-23 -9 t-23 9t-9 23v96q0 14 9 23t23 9t23 -9t9 -23zM1751 1449l-91 -90q-10 -10 -22 -10q-13 0 -23 10q-10 9 -10 22.5t10 22.5l90 91q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M609 720l287 208l287 -208l-109 -336h-355zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM1515 186q149 203 149 454v3l-102 -89l-240 224l63 323 l134 -12q-150 206 -389 282l53 -124l-287 -159l-287 159l53 124q-239 -76 -389 -282l135 12l62 -323l-240 -224l-102 89v-3q0 -251 149 -454l30 132l326 -40l139 -298l-116 -69q117 -39 240 -39t240 39l-116 69l139 298l326 40z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M448 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM256 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM832 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM66 768q-28 0 -47 19t-19 46v129h514v-129q0 -27 -19 -46t-46 -19h-383zM1216 224v-192q0 -14 -9 -23t-23 -9h-192 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1600 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23 zM1408 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1016v-13h-514v10q0 104 -382 102q-382 -1 -382 -102v-10h-514v13q0 17 8.5 43t34 64t65.5 75.5t110.5 76t160 67.5t224 47.5t293.5 18.5t293 -18.5t224 -47.5 t160.5 -67.5t110.5 -76t65.5 -75.5t34 -64t8.5 -43zM1792 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 962v-129q0 -27 -19 -46t-46 -19h-384q-27 0 -46 19t-19 46v129h514z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M704 1216v-768q0 -26 -19 -45t-45 -19v-576q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v512l249 873q7 23 31 23h424zM1024 1216v-704h-256v704h256zM1792 320v-512q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v576q-26 0 -45 19t-19 45v768h424q24 0 31 -23z M736 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23zM1408 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1755 1083q37 -37 37 -90t-37 -91l-401 -400l150 -150l-160 -160q-163 -163 -389.5 -186.5t-411.5 100.5l-362 -362h-181v181l362 362q-124 185 -100.5 411.5t186.5 389.5l160 160l150 -150l400 401q38 37 91 37t90 -37t37 -90.5t-37 -90.5l-400 -401l234 -234l401 400 q38 37 91 37t90 -37z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M873 796q0 -83 -63.5 -142.5t-152.5 -59.5t-152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59t152.5 -59t63.5 -143zM1375 796q0 -83 -63 -142.5t-153 -59.5q-89 0 -152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59q90 0 153 -59t63 -143zM1600 616v667q0 87 -32 123.5 t-111 36.5h-1112q-83 0 -112.5 -34t-29.5 -126v-673q43 -23 88.5 -40t81 -28t81 -18.5t71 -11t70 -4t58.5 -0.5t56.5 2t44.5 2q68 1 95 -27q6 -6 10 -9q26 -25 61 -51q7 91 118 87q5 0 36.5 -1.5t43 -2t45.5 -1t53 1t54.5 4.5t61 8.5t62 13.5t67 19.5t67.5 27t72 34.5z M1763 621q-121 -149 -372 -252q84 -285 -23 -465q-66 -113 -183 -148q-104 -32 -182 15q-86 51 -82 164l-1 326v1q-8 2 -24.5 6t-23.5 5l-1 -338q4 -114 -83 -164q-79 -47 -183 -15q-117 36 -182 150q-105 180 -22 463q-251 103 -372 252q-25 37 -4 63t60 -1q3 -2 11 -7 t11 -8v694q0 72 47 123t114 51h1257q67 0 114 -51t47 -123v-694l21 15q39 27 60 1t-4 -63z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M896 1102v-434h-145v434h145zM1294 1102v-434h-145v434h145zM1294 342l253 254v795h-1194v-1049h326v-217l217 217h398zM1692 1536v-1013l-434 -434h-326l-217 -217h-217v217h-398v1158l109 289h1483z" />
|
||||
<glyph unicode="" d="M773 217v-127q-1 -292 -6 -305q-12 -32 -51 -40q-54 -9 -181.5 38t-162.5 89q-13 15 -17 36q-1 12 4 26q4 10 34 47t181 216q1 0 60 70q15 19 39.5 24.5t49.5 -3.5q24 -10 37.5 -29t12.5 -42zM624 468q-3 -55 -52 -70l-120 -39q-275 -88 -292 -88q-35 2 -54 36 q-12 25 -17 75q-8 76 1 166.5t30 124.5t56 32q13 0 202 -77q70 -29 115 -47l84 -34q23 -9 35.5 -30.5t11.5 -48.5zM1450 171q-7 -54 -91.5 -161t-135.5 -127q-37 -14 -63 7q-14 10 -184 287l-47 77q-14 21 -11.5 46t19.5 46q35 43 83 26q1 -1 119 -40q203 -66 242 -79.5 t47 -20.5q28 -22 22 -61zM778 803q5 -102 -54 -122q-58 -17 -114 71l-378 598q-8 35 19 62q41 43 207.5 89.5t224.5 31.5q40 -10 49 -45q3 -18 22 -305.5t24 -379.5zM1440 695q3 -39 -26 -59q-15 -10 -329 -86q-67 -15 -91 -23l1 2q-23 -6 -46 4t-37 32q-30 47 0 87 q1 1 75 102q125 171 150 204t34 39q28 19 65 2q48 -23 123 -133.5t81 -167.5v-3z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1024 1024h-384v-384h384v384zM1152 384v-128h-640v128h640zM1152 1152v-640h-640v640h640zM1792 384v-128h-512v128h512zM1792 640v-128h-512v128h512zM1792 896v-128h-512v128h512zM1792 1152v-128h-512v128h512zM256 192v960h-128v-960q0 -26 19 -45t45 -19t45 19 t19 45zM1920 192v1088h-1536v-1088q0 -33 -11 -64h1483q26 0 45 19t19 45zM2048 1408v-1216q0 -80 -56 -136t-136 -56h-1664q-80 0 -136 56t-56 136v1088h256v128h1792z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1024 13q-20 0 -93 73.5t-73 93.5q0 32 62.5 54t103.5 22t103.5 -22t62.5 -54q0 -20 -73 -93.5t-93 -73.5zM1294 284q-2 0 -40 25t-101.5 50t-128.5 25t-128.5 -25t-101 -50t-40.5 -25q-18 0 -93.5 75t-75.5 93q0 13 10 23q78 77 196 121t233 44t233 -44t196 -121 q10 -10 10 -23q0 -18 -75.5 -93t-93.5 -75zM1567 556q-11 0 -23 8q-136 105 -252 154.5t-268 49.5q-85 0 -170.5 -22t-149 -53t-113.5 -62t-79 -53t-31 -22q-17 0 -92 75t-75 93q0 12 10 22q132 132 320 205t380 73t380 -73t320 -205q10 -10 10 -22q0 -18 -75 -93t-92 -75z M1838 827q-11 0 -22 9q-179 157 -371.5 236.5t-420.5 79.5t-420.5 -79.5t-371.5 -236.5q-11 -9 -22 -9q-17 0 -92.5 75t-75.5 93q0 13 10 23q187 186 445 288t527 102t527 -102t445 -288q10 -10 10 -23q0 -18 -75.5 -93t-92.5 -75z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M384 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5 t37.5 90.5zM384 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 768q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1536 0v384q0 52 -38 90t-90 38t-90 -38t-38 -90v-384q0 -52 38 -90t90 -38t90 38t38 90zM1152 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1536 1088v256q0 26 -19 45t-45 19h-1280q-26 0 -45 -19t-19 -45v-256q0 -26 19 -45t45 -19h1280q26 0 45 19t19 45zM1536 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1408v-1536q0 -52 -38 -90t-90 -38 h-1408q-52 0 -90 38t-38 90v1536q0 52 38 90t90 38h1408q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" d="M1519 890q18 -84 -4 -204q-87 -444 -565 -444h-44q-25 0 -44 -16.5t-24 -42.5l-4 -19l-55 -346l-2 -15q-5 -26 -24.5 -42.5t-44.5 -16.5h-251q-21 0 -33 15t-9 36q9 56 26.5 168t26.5 168t27 167.5t27 167.5q5 37 43 37h131q133 -2 236 21q175 39 287 144q102 95 155 246 q24 70 35 133q1 6 2.5 7.5t3.5 1t6 -3.5q79 -59 98 -162zM1347 1172q0 -107 -46 -236q-80 -233 -302 -315q-113 -40 -252 -42q0 -1 -90 -1l-90 1q-100 0 -118 -96q-2 -8 -85 -530q-1 -10 -12 -10h-295q-22 0 -36.5 16.5t-11.5 38.5l232 1471q5 29 27.5 48t51.5 19h598 q34 0 97.5 -13t111.5 -32q107 -41 163.5 -123t56.5 -196z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M441 864q32 0 52 -26q266 -364 362 -774h-446q-127 441 -367 749q-12 16 -3 33.5t29 17.5h373zM1000 507q-49 -199 -125 -393q-79 310 -256 594q40 221 44 449q211 -340 337 -650zM1099 1216q235 -324 384.5 -698.5t184.5 -773.5h-451q-41 665 -553 1472h435zM1792 640 q0 -424 -101 -812q-67 560 -359 1083q-25 301 -106 584q-4 16 5.5 28.5t25.5 12.5h359q21 0 38.5 -13t22.5 -33q115 -409 115 -850z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1975 546h-138q14 37 66 179l3 9q4 10 10 26t9 26l12 -55zM531 611l-58 295q-11 54 -75 54h-268l-2 -13q311 -79 403 -336zM710 960l-162 -438l-17 89q-26 70 -85 129.5t-131 88.5l135 -510h175l261 641h-176zM849 318h166l104 642h-166zM1617 944q-69 27 -149 27 q-123 0 -201 -59t-79 -153q-1 -102 145 -174q48 -23 67 -41t19 -39q0 -30 -30 -46t-69 -16q-86 0 -156 33l-22 11l-23 -144q74 -34 185 -34q130 -1 208.5 59t80.5 160q0 106 -140 174q-49 25 -71 42t-22 38q0 22 24.5 38.5t70.5 16.5q70 1 124 -24l15 -8zM2042 960h-128 q-65 0 -87 -54l-246 -588h174l35 96h212q5 -22 20 -96h154zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M671 603h-13q-47 0 -47 -32q0 -22 20 -22q17 0 28 15t12 39zM1066 639h62v3q1 4 0.5 6.5t-1 7t-2 8t-4.5 6.5t-7.5 5t-11.5 2q-28 0 -36 -38zM1606 603h-12q-48 0 -48 -32q0 -22 20 -22q17 0 28 15t12 39zM1925 629q0 41 -30 41q-19 0 -31 -20t-12 -51q0 -42 28 -42 q20 0 32.5 20t12.5 52zM480 770h87l-44 -262h-56l32 201l-71 -201h-39l-4 200l-34 -200h-53l44 262h81l2 -163zM733 663q0 -6 -4 -42q-16 -101 -17 -113h-47l1 22q-20 -26 -58 -26q-23 0 -37.5 16t-14.5 42q0 39 26 60.5t73 21.5q14 0 23 -1q0 3 0.5 5.5t1 4.5t0.5 3 q0 20 -36 20q-29 0 -59 -10q0 4 7 48q38 11 67 11q74 0 74 -62zM889 721l-8 -49q-22 3 -41 3q-27 0 -27 -17q0 -8 4.5 -12t21.5 -11q40 -19 40 -60q0 -72 -87 -71q-34 0 -58 6q0 2 7 49q29 -8 51 -8q32 0 32 19q0 7 -4.5 11.5t-21.5 12.5q-43 20 -43 59q0 72 84 72 q30 0 50 -4zM977 721h28l-7 -52h-29q-2 -17 -6.5 -40.5t-7 -38.5t-2.5 -18q0 -16 19 -16q8 0 16 2l-8 -47q-21 -7 -40 -7q-43 0 -45 47q0 12 8 56q3 20 25 146h55zM1180 648q0 -23 -7 -52h-111q-3 -22 10 -33t38 -11q30 0 58 14l-9 -54q-30 -8 -57 -8q-95 0 -95 95 q0 55 27.5 90.5t69.5 35.5q35 0 55.5 -21t20.5 -56zM1319 722q-13 -23 -22 -62q-22 2 -31 -24t-25 -128h-56l3 14q22 130 29 199h51l-3 -33q14 21 25.5 29.5t28.5 4.5zM1506 763l-9 -57q-28 14 -50 14q-31 0 -51 -27.5t-20 -70.5q0 -30 13.5 -47t38.5 -17q21 0 48 13 l-10 -59q-28 -8 -50 -8q-45 0 -71.5 30.5t-26.5 82.5q0 70 35.5 114.5t91.5 44.5q26 0 61 -13zM1668 663q0 -18 -4 -42q-13 -79 -17 -113h-46l1 22q-20 -26 -59 -26q-23 0 -37 16t-14 42q0 39 25.5 60.5t72.5 21.5q15 0 23 -1q2 7 2 13q0 20 -36 20q-29 0 -59 -10q0 4 8 48 q38 11 67 11q73 0 73 -62zM1809 722q-14 -24 -21 -62q-23 2 -31.5 -23t-25.5 -129h-56l3 14q19 104 29 199h52q0 -11 -4 -33q15 21 26.5 29.5t27.5 4.5zM1950 770h56l-43 -262h-53l3 19q-23 -23 -52 -23q-31 0 -49.5 24t-18.5 64q0 53 27.5 92t64.5 39q31 0 53 -29z M2061 640q0 148 -72.5 273t-198 198t-273.5 73q-181 0 -328 -110q127 -116 171 -284h-50q-44 150 -158 253q-114 -103 -158 -253h-50q44 168 171 284q-147 110 -328 110q-148 0 -273.5 -73t-198 -198t-72.5 -273t72.5 -273t198 -198t273.5 -73q181 0 328 110 q-120 111 -165 264h50q46 -138 152 -233q106 95 152 233h50q-45 -153 -165 -264q147 -110 328 -110q148 0 273.5 73t198 198t72.5 273zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M313 759q0 -51 -36 -84q-29 -26 -89 -26h-17v220h17q61 0 89 -27q36 -31 36 -83zM2089 824q0 -52 -64 -52h-19v101h20q63 0 63 -49zM380 759q0 74 -50 120.5t-129 46.5h-95v-333h95q74 0 119 38q60 51 60 128zM410 593h65v333h-65v-333zM730 694q0 40 -20.5 62t-75.5 42 q-29 10 -39.5 19t-10.5 23q0 16 13.5 26.5t34.5 10.5q29 0 53 -27l34 44q-41 37 -98 37q-44 0 -74 -27.5t-30 -67.5q0 -35 18 -55.5t64 -36.5q37 -13 45 -19q19 -12 19 -34q0 -20 -14 -33.5t-36 -13.5q-48 0 -71 44l-42 -40q44 -64 115 -64q51 0 83 30.5t32 79.5zM1008 604 v77q-37 -37 -78 -37q-49 0 -80.5 32.5t-31.5 82.5q0 48 31.5 81.5t77.5 33.5q43 0 81 -38v77q-40 20 -80 20q-74 0 -125.5 -50.5t-51.5 -123.5t51 -123.5t125 -50.5q42 0 81 19zM2240 0v527q-65 -40 -144.5 -84t-237.5 -117t-329.5 -137.5t-417.5 -134.5t-504 -118h1569 q26 0 45 19t19 45zM1389 757q0 75 -53 128t-128 53t-128 -53t-53 -128t53 -128t128 -53t128 53t53 128zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56h-115v74h119v57h-184v-333zM2105 593h80l-105 140q76 16 76 94q0 47 -31 73 t-87 26h-97v-333h65v133h9zM2304 1274v-1268q0 -56 -38.5 -95t-93.5 -39h-2040q-55 0 -93.5 39t-38.5 95v1268q0 56 38.5 95t93.5 39h2040q55 0 93.5 -39t38.5 -95z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M119 854h89l-45 108zM740 328l74 79l-70 79h-163v-49h142v-55h-142v-54h159zM898 406l99 -110v217zM1186 453q0 33 -40 33h-84v-69h83q41 0 41 36zM1475 457q0 29 -42 29h-82v-61h81q43 0 43 32zM1197 923q0 29 -42 29h-82v-60h81q43 0 43 31zM1656 854h89l-44 108z M699 1009v-271h-66v212l-94 -212h-57l-94 212v-212h-132l-25 60h-135l-25 -60h-70l116 271h96l110 -257v257h106l85 -184l77 184h108zM1255 453q0 -20 -5.5 -35t-14 -25t-22.5 -16.5t-26 -10t-31.5 -4.5t-31.5 -1t-32.5 0.5t-29.5 0.5v-91h-126l-80 90l-83 -90h-256v271h260 l80 -89l82 89h207q109 0 109 -89zM964 794v-56h-217v271h217v-57h-152v-49h148v-55h-148v-54h152zM2304 235v-229q0 -55 -38.5 -94.5t-93.5 -39.5h-2040q-55 0 -93.5 39.5t-38.5 94.5v678h111l25 61h55l25 -61h218v46l19 -46h113l20 47v-47h541v99l10 1q10 0 10 -14v-86h279 v23q23 -12 55 -18t52.5 -6.5t63 0.5t51.5 1l25 61h56l25 -61h227v58l34 -58h182v378h-180v-44l-25 44h-185v-44l-23 44h-249q-69 0 -109 -22v22h-172v-22q-24 22 -73 22h-628l-43 -97l-43 97h-198v-44l-22 44h-169l-78 -179v391q0 55 38.5 94.5t93.5 39.5h2040 q55 0 93.5 -39.5t38.5 -94.5v-678h-120q-51 0 -81 -22v22h-177q-55 0 -78 -22v22h-316v-22q-31 22 -87 22h-209v-22q-23 22 -91 22h-234l-54 -58l-50 58h-349v-378h343l55 59l52 -59h211v89h21q59 0 90 13v-102h174v99h8q8 0 10 -2t2 -10v-87h529q57 0 88 24v-24h168 q60 0 95 17zM1546 469q0 -23 -12 -43t-34 -29q25 -9 34 -26t9 -46v-54h-65v45q0 33 -12 43.5t-46 10.5h-69v-99h-65v271h154q48 0 77 -15t29 -58zM1269 936q0 -24 -12.5 -44t-33.5 -29q26 -9 34.5 -25.5t8.5 -46.5v-53h-65q0 9 0.5 26.5t0 25t-3 18.5t-8.5 16t-17.5 8.5 t-29.5 3.5h-70v-98h-64v271l153 -1q49 0 78 -14.5t29 -57.5zM1798 327v-56h-216v271h216v-56h-151v-49h148v-55h-148v-54zM1372 1009v-271h-66v271h66zM2065 357q0 -86 -102 -86h-126v58h126q34 0 34 25q0 16 -17 21t-41.5 5t-49.5 3.5t-42 22.5t-17 55q0 39 26 60t66 21 h130v-57h-119q-36 0 -36 -25q0 -16 17.5 -20.5t42 -4t49 -2.5t42 -21.5t17.5 -54.5zM2304 407v-101q-24 -35 -88 -35h-125v58h125q33 0 33 25q0 13 -12.5 19t-31 5.5t-40 2t-40 8t-31 24t-12.5 48.5q0 39 26.5 60t66.5 21h129v-57h-118q-36 0 -36 -25q0 -20 29 -22t68.5 -5 t56.5 -26zM2139 1008v-270h-92l-122 203v-203h-132l-26 60h-134l-25 -60h-75q-129 0 -129 133q0 138 133 138h63v-59q-7 0 -28 1t-28.5 0.5t-23 -2t-21.5 -6.5t-14.5 -13.5t-11.5 -23t-3 -33.5q0 -38 13.5 -58t49.5 -20h29l92 213h97l109 -256v256h99l114 -188v188h66z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M745 630q0 -37 -25.5 -61.5t-62.5 -24.5q-29 0 -46.5 16t-17.5 44q0 37 25 62.5t62 25.5q28 0 46.5 -16.5t18.5 -45.5zM1530 779q0 -42 -22 -57t-66 -15l-32 -1l17 107q2 11 13 11h18q22 0 35 -2t25 -12.5t12 -30.5zM1881 630q0 -36 -25.5 -61t-61.5 -25q-29 0 -47 16 t-18 44q0 37 25 62.5t62 25.5q28 0 46.5 -16.5t18.5 -45.5zM513 801q0 59 -38.5 85.5t-100.5 26.5h-160q-19 0 -21 -19l-65 -408q-1 -6 3 -11t10 -5h76q20 0 22 19l18 110q1 8 7 13t15 6.5t17 1.5t19 -1t14 -1q86 0 135 48.5t49 134.5zM822 489l41 261q1 6 -3 11t-10 5h-76 q-14 0 -17 -33q-27 40 -95 40q-72 0 -122.5 -54t-50.5 -127q0 -59 34.5 -94t92.5 -35q28 0 58 12t48 32q-4 -12 -4 -21q0 -16 13 -16h69q19 0 22 19zM1269 752q0 5 -4 9.5t-9 4.5h-77q-11 0 -18 -10l-106 -156l-44 150q-5 16 -22 16h-75q-5 0 -9 -4.5t-4 -9.5q0 -2 19.5 -59 t42 -123t23.5 -70q-82 -112 -82 -120q0 -13 13 -13h77q11 0 18 10l255 368q2 2 2 7zM1649 801q0 59 -38.5 85.5t-100.5 26.5h-159q-20 0 -22 -19l-65 -408q-1 -6 3 -11t10 -5h82q12 0 16 13l18 116q1 8 7 13t15 6.5t17 1.5t19 -1t14 -1q86 0 135 48.5t49 134.5zM1958 489 l41 261q1 6 -3 11t-10 5h-76q-14 0 -17 -33q-26 40 -95 40q-72 0 -122.5 -54t-50.5 -127q0 -59 34.5 -94t92.5 -35q29 0 59 12t47 32q0 -1 -2 -9t-2 -12q0 -16 13 -16h69q19 0 22 19zM2176 898v1q0 14 -13 14h-74q-11 0 -13 -11l-65 -416l-1 -2q0 -5 4 -9.5t10 -4.5h66 q19 0 21 19zM392 764q-5 -35 -26 -46t-60 -11l-33 -1l17 107q2 11 13 11h19q40 0 58 -11.5t12 -48.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1597 633q0 -69 -21 -106q-19 -35 -52 -35q-23 0 -41 9v224q29 30 57 30q57 0 57 -122zM2035 669h-110q6 98 56 98q51 0 54 -98zM476 534q0 59 -33 91.5t-101 57.5q-36 13 -52 24t-16 25q0 26 38 26q58 0 124 -33l18 112q-67 32 -149 32q-77 0 -123 -38q-48 -39 -48 -109 q0 -58 32.5 -90.5t99.5 -56.5q39 -14 54.5 -25.5t15.5 -27.5q0 -31 -48 -31q-29 0 -70 12.5t-72 30.5l-18 -113q72 -41 168 -41q81 0 129 37q51 41 51 117zM771 749l19 111h-96v135l-129 -21l-18 -114l-46 -8l-17 -103h62v-219q0 -84 44 -120q38 -30 111 -30q32 0 79 11v118 q-32 -7 -44 -7q-42 0 -42 50v197h77zM1087 724v139q-15 3 -28 3q-32 0 -55.5 -16t-33.5 -46l-10 56h-131v-471h150v306q26 31 82 31q16 0 26 -2zM1124 389h150v471h-150v-471zM1746 638q0 122 -45 179q-40 52 -111 52q-64 0 -117 -56l-8 47h-132v-645l150 25v151 q36 -11 68 -11q83 0 134 56q61 65 61 202zM1278 986q0 33 -23 56t-56 23t-56 -23t-23 -56t23 -56.5t56 -23.5t56 23.5t23 56.5zM2176 629q0 113 -48 176q-50 64 -144 64q-96 0 -151.5 -66t-55.5 -180q0 -128 63 -188q55 -55 161 -55q101 0 160 40l-16 103q-57 -31 -128 -31 q-43 0 -63 19q-23 19 -28 66h248q2 14 2 52zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1558 684q61 -356 298 -556q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5zM1024 -176q16 0 16 16t-16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5zM2026 1424q8 -10 7.5 -23.5t-10.5 -22.5 l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5 l418 363q10 8 23.5 7t21.5 -11z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1040 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM503 315l877 760q-42 88 -132.5 146.5t-223.5 58.5q-93 0 -169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -384 -137 -645zM1856 128 q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5l149 129h757q-166 187 -227 459l111 97q61 -356 298 -556zM1942 1520l84 -96q8 -10 7.5 -23.5t-10.5 -22.5l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161 q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5l418 363q10 8 23.5 7t21.5 -11z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M512 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM768 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1024 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167 q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" d="M1150 462v-109q0 -50 -36.5 -89t-94 -60.5t-118 -32.5t-117.5 -11q-205 0 -342.5 139t-137.5 346q0 203 136 339t339 136q34 0 75.5 -4.5t93 -18t92.5 -34t69 -56.5t28 -81v-109q0 -16 -16 -16h-118q-16 0 -16 16v70q0 43 -65.5 67.5t-137.5 24.5q-140 0 -228.5 -91.5 t-88.5 -237.5q0 -151 91.5 -249.5t233.5 -98.5q68 0 138 24t70 66v70q0 7 4.5 11.5t10.5 4.5h119q6 0 11 -4.5t5 -11.5zM768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" d="M972 761q0 108 -53.5 169t-147.5 61q-63 0 -124 -30.5t-110 -84.5t-79.5 -137t-30.5 -180q0 -112 53.5 -173t150.5 -61q96 0 176 66.5t122.5 166t42.5 203.5zM1536 640q0 -111 -37 -197t-98.5 -135t-131.5 -74.5t-145 -27.5q-6 0 -15.5 -0.5t-16.5 -0.5q-95 0 -142 53 q-28 33 -33 83q-52 -66 -131.5 -110t-173.5 -44q-161 0 -249.5 95.5t-88.5 269.5q0 157 66 290t179 210.5t246 77.5q87 0 155 -35.5t106 -99.5l2 19l11 56q1 6 5.5 12t9.5 6h118q5 0 13 -11q5 -5 3 -16l-120 -614q-5 -24 -5 -48q0 -39 12.5 -52t44.5 -13q28 1 57 5.5t73 24 t77 50t57 89.5t24 137q0 292 -174 466t-466 174q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51q228 0 405 144q11 9 24 8t21 -12l41 -49q8 -12 7 -24q-2 -13 -12 -22q-102 -83 -227.5 -128t-258.5 -45q-156 0 -298 61 t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q344 0 556 -212t212 -556z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1698 1442q94 -94 94 -226.5t-94 -225.5l-225 -223l104 -104q10 -10 10 -23t-10 -23l-210 -210q-10 -10 -23 -10t-23 10l-105 105l-603 -603q-37 -37 -90 -37h-203l-256 -128l-64 64l128 256v203q0 53 37 90l603 603l-105 105q-10 10 -10 23t10 23l210 210q10 10 23 10 t23 -10l104 -104l223 225q93 94 225.5 94t226.5 -94zM512 64l576 576l-192 192l-576 -576v-192h192z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1615 1536q70 0 122.5 -46.5t52.5 -116.5q0 -63 -45 -151q-332 -629 -465 -752q-97 -91 -218 -91q-126 0 -216.5 92.5t-90.5 219.5q0 128 92 212l638 579q59 54 130 54zM706 502q39 -76 106.5 -130t150.5 -76l1 -71q4 -213 -129.5 -347t-348.5 -134q-123 0 -218 46.5 t-152.5 127.5t-86.5 183t-29 220q7 -5 41 -30t62 -44.5t59 -36.5t46 -17q41 0 55 37q25 66 57.5 112.5t69.5 76t88 47.5t103 25.5t125 10.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 128v-384h-1792v384q45 0 85 14t59 27.5t47 37.5q30 27 51.5 38t56.5 11t55.5 -11t52.5 -38q29 -25 47 -38t58 -27t86 -14q45 0 85 14.5t58 27t48 37.5q21 19 32.5 27t31 15t43.5 7q35 0 56.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14t85 14t59 27.5t47 37.5 q30 27 51.5 38t56.5 11q34 0 55.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14zM1792 448v-192q-35 0 -55.5 11t-52.5 38q-29 25 -47 38t-58 27t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-22 -19 -33 -27t-31 -15t-44 -7q-35 0 -56.5 11t-51.5 38q-29 25 -47 38t-58 27 t-86 14q-45 0 -85 -14.5t-58 -27t-48 -37.5q-21 -19 -32.5 -27t-31 -15t-43.5 -7q-35 0 -56.5 11t-51.5 38q-28 24 -47 37.5t-59 27.5t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-30 -27 -51.5 -38t-56.5 -11v192q0 80 56 136t136 56h64v448h256v-448h256v448h256v-448h256v448 h256v-448h64q80 0 136 -56t56 -136zM512 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1024 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51 t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1536 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1664 1024l256 -896h-1664v576l448 576l576 -576z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M768 646l546 -546q-106 -108 -247.5 -168t-298.5 -60q-209 0 -385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103v-762zM955 640h773q0 -157 -60 -298.5t-168 -247.5zM1664 768h-768v768q209 0 385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1920 1248v-435q0 -21 -19.5 -29.5t-35.5 7.5l-121 121l-633 -633q-10 -10 -23 -10t-23 10l-233 233l-416 -416l-192 192l585 585q10 10 23 10t23 -10l233 -233l464 464l-121 121q-16 16 -7.5 35.5t29.5 19.5h435q14 0 23 -9 t9 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1292 832q0 -6 10 -41q10 -29 25 -49.5t41 -34t44 -20t55 -16.5q325 -91 325 -332q0 -146 -105.5 -242.5t-254.5 -96.5q-59 0 -111.5 18.5t-91.5 45.5t-77 74.5t-63 87.5t-53.5 103.5t-43.5 103t-39.5 106.5t-35.5 95q-32 81 -61.5 133.5t-73.5 96.5t-104 64t-142 20 q-96 0 -183 -55.5t-138 -144.5t-51 -185q0 -160 106.5 -279.5t263.5 -119.5q177 0 258 95q56 63 83 116l84 -152q-15 -34 -44 -70l1 -1q-131 -152 -388 -152q-147 0 -269.5 79t-190.5 207.5t-68 274.5q0 105 43.5 206t116 176.5t172 121.5t204.5 46q87 0 159 -19t123.5 -50 t95 -80t72.5 -99t58.5 -117t50.5 -124.5t50 -130.5t55 -127q96 -200 233 -200q81 0 138.5 48.5t57.5 128.5q0 42 -19 72t-50.5 46t-72.5 31.5t-84.5 27t-87.5 34t-81 52t-65 82t-39 122.5q-3 16 -3 33q0 110 87.5 192t198.5 78q78 -3 120.5 -14.5t90.5 -53.5h-1 q12 -11 23 -24.5t26 -36t19 -27.5l-129 -99q-26 49 -54 70v1q-23 21 -97 21q-49 0 -84 -33t-35 -83z" />
|
||||
<glyph unicode="" d="M1432 484q0 173 -234 239q-35 10 -53 16.5t-38 25t-29 46.5q0 2 -2 8.5t-3 12t-1 7.5q0 36 24.5 59.5t60.5 23.5q54 0 71 -15h-1q20 -15 39 -51l93 71q-39 54 -49 64q-33 29 -67.5 39t-85.5 10q-80 0 -142 -57.5t-62 -137.5q0 -7 2 -23q16 -96 64.5 -140t148.5 -73 q29 -8 49 -15.5t45 -21.5t38.5 -34.5t13.5 -46.5v-5q1 -58 -40.5 -93t-100.5 -35q-97 0 -167 144q-23 47 -51.5 121.5t-48 125.5t-54 110.5t-74 95.5t-103.5 60.5t-147 24.5q-101 0 -192 -56t-144 -148t-50 -192v-1q4 -108 50.5 -199t133.5 -147.5t196 -56.5q186 0 279 110 q20 27 31 51l-60 109q-42 -80 -99 -116t-146 -36q-115 0 -191 87t-76 204q0 105 82 189t186 84q112 0 170 -53.5t104 -172.5q8 -21 25.5 -68.5t28.5 -76.5t31.5 -74.5t38.5 -74t45.5 -62.5t55.5 -53.5t66 -33t80 -13.5q107 0 183 69.5t76 174.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1152 640q0 104 -40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM1920 640q0 104 -40.5 198.5 t-109.5 163.5t-163.5 109.5t-198.5 40.5h-386q119 -90 188.5 -224t69.5 -288t-69.5 -288t-188.5 -224h386q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM2048 640q0 -130 -51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5 t-136.5 204t-51 248.5t51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M0 640q0 130 51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5t-51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5t-136.5 204t-51 248.5zM1408 128q104 0 198.5 40.5t163.5 109.5 t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M762 384h-314q-40 0 -57.5 35t6.5 67l188 251q-65 31 -137 31q-132 0 -226 -94t-94 -226t94 -226t226 -94q115 0 203 72.5t111 183.5zM576 512h186q-18 85 -75 148zM1056 512l288 384h-480l-99 -132q105 -103 126 -252h165zM2176 448q0 132 -94 226t-226 94 q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94t226 94t94 226zM2304 448q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 97 39.5 183.5t109.5 149.5l-65 98l-353 -469 q-18 -26 -51 -26h-197q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q114 0 215 -55l137 183h-224q-26 0 -45 19t-19 45t19 45t45 19h384v-128h435l-85 128h-222q-26 0 -45 19t-19 45t19 45t45 19h256q33 0 53 -28l267 -400 q91 44 192 44q185 0 316.5 -131.5t131.5 -316.5z" />
|
||||
<glyph unicode="" d="M384 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1362 716l-72 384q-5 23 -22.5 37.5t-40.5 14.5 h-918q-23 0 -40.5 -14.5t-22.5 -37.5l-72 -384q-5 -30 14 -53t49 -23h1062q30 0 49 23t14 53zM1136 1328q0 20 -14 34t-34 14h-640q-20 0 -34 -14t-14 -34t14 -34t34 -14h640q20 0 34 14t14 34zM1536 603v-603h-128v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5v128h-768v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5v128h-128v603q0 112 25 223l103 454q9 78 97.5 137t230 89t312.5 30t312.5 -30t230 -89t97.5 -137l105 -454q23 -102 23 -223z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1463 704q0 -35 -25 -60.5t-61 -25.5h-702q-36 0 -61 25.5t-25 60.5t25 60.5t61 25.5h702q36 0 61 -25.5t25 -60.5zM1677 704q0 86 -23 170h-982q-36 0 -61 25t-25 60q0 36 25 61t61 25h908q-88 143 -235 227t-320 84q-177 0 -327.5 -87.5t-238 -237.5t-87.5 -327 q0 -86 23 -170h982q36 0 61 -25t25 -60q0 -36 -25 -61t-61 -25h-908q88 -143 235.5 -227t320.5 -84q132 0 253 51.5t208 139t139 208t52 253.5zM2048 959q0 -35 -25 -60t-61 -25h-131q17 -85 17 -170q0 -167 -65.5 -319.5t-175.5 -263t-262.5 -176t-319.5 -65.5 q-246 0 -448.5 133t-301.5 350h-189q-36 0 -61 25t-25 61q0 35 25 60t61 25h132q-17 85 -17 170q0 167 65.5 319.5t175.5 263t262.5 176t320.5 65.5q245 0 447.5 -133t301.5 -350h188q36 0 61 -25t25 -61z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M953 1158l-114 -328l117 -21q165 451 165 518q0 56 -38 56q-57 0 -130 -225zM654 471l33 -88q37 42 71 67l-33 5.5t-38.5 7t-32.5 8.5zM362 1367q0 -98 159 -521q18 10 49 10q15 0 75 -5l-121 351q-75 220 -123 220q-19 0 -29 -17.5t-10 -37.5zM283 608q0 -36 51.5 -119 t117.5 -153t100 -70q14 0 25.5 13t11.5 27q0 24 -32 102q-13 32 -32 72t-47.5 89t-61.5 81t-62 32q-20 0 -45.5 -27t-25.5 -47zM125 273q0 -41 25 -104q59 -145 183.5 -227t281.5 -82q227 0 382 170q152 169 152 427q0 43 -1 67t-11.5 62t-30.5 56q-56 49 -211.5 75.5 t-270.5 26.5q-37 0 -49 -11q-12 -5 -12 -35q0 -34 21.5 -60t55.5 -40t77.5 -23.5t87.5 -11.5t85 -4t70 0h23q24 0 40 -19q15 -19 19 -55q-28 -28 -96 -54q-61 -22 -93 -46q-64 -46 -108.5 -114t-44.5 -137q0 -31 18.5 -88.5t18.5 -87.5l-3 -12q-4 -12 -4 -14 q-137 10 -146 216q-8 -2 -41 -2q2 -7 2 -21q0 -53 -40.5 -89.5t-94.5 -36.5q-82 0 -166.5 78t-84.5 159q0 34 33 67q52 -64 60 -76q77 -104 133 -104q12 0 26.5 8.5t14.5 20.5q0 34 -87.5 145t-116.5 111q-43 0 -70 -44.5t-27 -90.5zM11 264q0 101 42.5 163t136.5 88 q-28 74 -28 104q0 62 61 123t122 61q29 0 70 -15q-163 462 -163 567q0 80 41 130.5t119 50.5q131 0 325 -581q6 -17 8 -23q6 16 29 79.5t43.5 118.5t54 127.5t64.5 123t70.5 86.5t76.5 36q71 0 112 -49t41 -122q0 -108 -159 -550q61 -15 100.5 -46t58.5 -78t26 -93.5 t7 -110.5q0 -150 -47 -280t-132 -225t-211 -150t-278 -55q-111 0 -223 42q-149 57 -258 191.5t-109 286.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M785 528h207q-14 -158 -98.5 -248.5t-214.5 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-203q-5 64 -35.5 99t-81.5 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t40 -51.5t66 -18q95 0 109 139zM1497 528h206 q-14 -158 -98 -248.5t-214 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-204q-4 64 -35 99t-81 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t39.5 -51.5t65.5 -18q49 0 76.5 38t33.5 101zM1856 647q0 207 -15.5 307 t-60.5 161q-6 8 -13.5 14t-21.5 15t-16 11q-86 63 -697 63q-625 0 -710 -63q-5 -4 -17.5 -11.5t-21 -14t-14.5 -14.5q-45 -60 -60 -159.5t-15 -308.5q0 -208 15 -307.5t60 -160.5q6 -8 15 -15t20.5 -14t17.5 -12q44 -33 239.5 -49t470.5 -16q610 0 697 65q5 4 17 11t20.5 14 t13.5 16q46 60 61 159t15 309zM2048 1408v-1536h-2048v1536h2048z" />
|
||||
<glyph unicode="" d="M992 912v-496q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v496q0 112 -80 192t-192 80h-272v-1152q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v1344q0 14 9 23t23 9h464q135 0 249 -66.5t180.5 -180.5t66.5 -249zM1376 1376v-880q0 -135 -66.5 -249t-180.5 -180.5 t-249 -66.5h-464q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h160q14 0 23 -9t9 -23v-768h272q112 0 192 80t80 192v880q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" />
|
||||
<glyph unicode="" d="M1311 694v-114q0 -24 -13.5 -38t-37.5 -14h-202q-24 0 -38 14t-14 38v114q0 24 14 38t38 14h202q24 0 37.5 -14t13.5 -38zM821 464v250q0 53 -32.5 85.5t-85.5 32.5h-133q-68 0 -96 -52q-28 52 -96 52h-130q-53 0 -85.5 -32.5t-32.5 -85.5v-250q0 -22 21 -22h55 q22 0 22 22v230q0 24 13.5 38t38.5 14h94q24 0 38 -14t14 -38v-230q0 -22 21 -22h54q22 0 22 22v230q0 24 14 38t38 14h97q24 0 37.5 -14t13.5 -38v-230q0 -22 22 -22h55q21 0 21 22zM1410 560v154q0 53 -33 85.5t-86 32.5h-264q-53 0 -86 -32.5t-33 -85.5v-410 q0 -21 22 -21h55q21 0 21 21v180q31 -42 94 -42h191q53 0 86 32.5t33 85.5zM1536 1176v-1072q0 -96 -68 -164t-164 -68h-1072q-96 0 -164 68t-68 164v1072q0 96 68 164t164 68h1072q96 0 164 -68t68 -164z" />
|
||||
<glyph unicode="" d="M915 450h-294l147 551zM1001 128h311l-324 1024h-440l-324 -1024h311l383 314zM1536 1120v-960q0 -118 -85 -203t-203 -85h-960q-118 0 -203 85t-85 203v960q0 118 85 203t203 85h960q118 0 203 -85t85 -203z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M2048 641q0 -21 -13 -36.5t-33 -19.5l-205 -356q3 -9 3 -18q0 -20 -12.5 -35.5t-32.5 -19.5l-193 -337q3 -8 3 -16q0 -23 -16.5 -40t-40.5 -17q-25 0 -41 18h-400q-17 -20 -43 -20t-43 20h-399q-17 -20 -43 -20q-23 0 -40 16.5t-17 40.5q0 8 4 20l-193 335 q-20 4 -32.5 19.5t-12.5 35.5q0 9 3 18l-206 356q-20 5 -32.5 20.5t-12.5 35.5q0 21 13.5 36.5t33.5 19.5l199 344q0 1 -0.5 3t-0.5 3q0 36 34 51l209 363q-4 10 -4 18q0 24 17 40.5t40 16.5q26 0 44 -21h396q16 21 43 21t43 -21h398q18 21 44 21q23 0 40 -16.5t17 -40.5 q0 -6 -4 -18l207 -358q23 -1 39 -17.5t16 -38.5q0 -13 -7 -27l187 -324q19 -4 31.5 -19.5t12.5 -35.5zM1063 -158h389l-342 354h-143l-342 -354h360q18 16 39 16t39 -16zM112 654q1 -4 1 -13q0 -10 -2 -15l208 -360q2 0 4.5 -1t5.5 -2.5l5 -2.5l188 199v347l-187 194 q-13 -8 -29 -10zM986 1438h-388l190 -200l554 200h-280q-16 -16 -38 -16t-38 16zM1689 226q1 6 5 11l-64 68l-17 -79h76zM1583 226l22 105l-252 266l-296 -307l63 -64h463zM1495 -142l16 28l65 310h-427l333 -343q8 4 13 5zM578 -158h5l342 354h-373v-335l4 -6q14 -5 22 -13 zM552 226h402l64 66l-309 321l-157 -166v-221zM359 226h163v189l-168 -177q4 -8 5 -12zM358 1051q0 -1 0.5 -2t0.5 -2q0 -16 -8 -29l171 -177v269zM552 1121v-311l153 -157l297 314l-223 236zM556 1425l-4 -8v-264l205 74l-191 201q-6 -2 -10 -3zM1447 1438h-16l-621 -224 l213 -225zM1023 946l-297 -315l311 -319l296 307zM688 634l-136 141v-284zM1038 270l-42 -44h85zM1374 618l238 -251l132 624l-3 5l-1 1zM1718 1018q-8 13 -8 29v2l-216 376q-5 1 -13 5l-437 -463l310 -327zM522 1142v223l-163 -282zM522 196h-163l163 -283v283zM1607 196 l-48 -227l130 227h-82zM1729 266l207 361q-2 10 -2 14q0 1 3 16l-171 296l-129 -612l77 -82q5 3 15 7z" />
|
||||
<glyph unicode="" d="M0 856q0 131 91.5 226.5t222.5 95.5h742l352 358v-1470q0 -132 -91.5 -227t-222.5 -95h-780q-131 0 -222.5 95t-91.5 227v790zM1232 102l-176 180v425q0 46 -32 79t-78 33h-484q-46 0 -78 -33t-32 -79v-492q0 -46 32.5 -79.5t77.5 -33.5h770z" />
|
||||
<glyph unicode="" d="M934 1386q-317 -121 -556 -362.5t-358 -560.5q-20 89 -20 176q0 208 102.5 384.5t278.5 279t384 102.5q82 0 169 -19zM1203 1267q93 -65 164 -155q-389 -113 -674.5 -400.5t-396.5 -676.5q-93 72 -155 162q112 386 395 671t667 399zM470 -67q115 356 379.5 622t619.5 384 q40 -92 54 -195q-292 -120 -516 -345t-343 -518q-103 14 -194 52zM1536 -125q-193 50 -367 115q-135 -84 -290 -107q109 205 274 370.5t369 275.5q-21 -152 -101 -284q65 -175 115 -370z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1893 1144l155 -1272q-131 0 -257 57q-200 91 -393 91q-226 0 -374 -148q-148 148 -374 148q-193 0 -393 -91q-128 -57 -252 -57h-5l155 1272q224 127 482 127q233 0 387 -106q154 106 387 106q258 0 482 -127zM1398 157q129 0 232 -28.5t260 -93.5l-124 1021 q-171 78 -368 78q-224 0 -374 -141q-150 141 -374 141q-197 0 -368 -78l-124 -1021q105 43 165.5 65t148.5 39.5t178 17.5q202 0 374 -108q172 108 374 108zM1438 191l-55 907q-211 -4 -359 -155q-152 155 -374 155q-176 0 -336 -66l-114 -941q124 51 228.5 76t221.5 25 q209 0 374 -102q172 107 374 102z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1500 165v733q0 21 -15 36t-35 15h-93q-20 0 -35 -15t-15 -36v-733q0 -20 15 -35t35 -15h93q20 0 35 15t15 35zM1216 165v531q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-531q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM924 165v429q0 20 -15 35t-35 15h-101 q-20 0 -35 -15t-15 -35v-429q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM632 165v362q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-362q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM2048 311q0 -166 -118 -284t-284 -118h-1244q-166 0 -284 118t-118 284 q0 116 63 214.5t168 148.5q-10 34 -10 73q0 113 80.5 193.5t193.5 80.5q102 0 180 -67q45 183 194 300t338 117q149 0 275 -73.5t199.5 -199.5t73.5 -275q0 -66 -14 -122q135 -33 221 -142.5t86 -247.5z" />
|
||||
<glyph unicode="" d="M0 1536h1536v-1392l-776 -338l-760 338v1392zM1436 209v926h-1336v-926l661 -294zM1436 1235v201h-1336v-201h1336zM181 937v-115h-37v115h37zM181 789v-115h-37v115h37zM181 641v-115h-37v115h37zM181 493v-115h-37v115h37zM181 345v-115h-37v115h37zM207 202l15 34 l105 -47l-15 -33zM343 142l15 34l105 -46l-15 -34zM478 82l15 34l105 -46l-15 -34zM614 23l15 33l104 -46l-15 -34zM797 10l105 46l15 -33l-105 -47zM932 70l105 46l15 -34l-105 -46zM1068 130l105 46l15 -34l-105 -46zM1203 189l105 47l15 -34l-105 -46zM259 1389v-36h-114 v36h114zM421 1389v-36h-115v36h115zM583 1389v-36h-115v36h115zM744 1389v-36h-114v36h114zM906 1389v-36h-114v36h114zM1068 1389v-36h-115v36h115zM1230 1389v-36h-115v36h115zM1391 1389v-36h-114v36h114zM181 1049v-79h-37v115h115v-36h-78zM421 1085v-36h-115v36h115z M583 1085v-36h-115v36h115zM744 1085v-36h-114v36h114zM906 1085v-36h-114v36h114zM1068 1085v-36h-115v36h115zM1230 1085v-36h-115v36h115zM1355 970v79h-78v36h115v-115h-37zM1355 822v115h37v-115h-37zM1355 674v115h37v-115h-37zM1355 526v115h37v-115h-37zM1355 378 v115h37v-115h-37zM1355 230v115h37v-115h-37zM760 265q-129 0 -221 91.5t-92 221.5q0 129 92 221t221 92q130 0 221.5 -92t91.5 -221q0 -130 -91.5 -221.5t-221.5 -91.5zM595 646q0 -36 19.5 -56.5t49.5 -25t64 -7t64 -2t49.5 -9t19.5 -30.5q0 -49 -112 -49q-97 0 -123 51 h-3l-31 -63q67 -42 162 -42q29 0 56.5 5t55.5 16t45.5 33t17.5 53q0 46 -27.5 69.5t-67.5 27t-79.5 3t-67 5t-27.5 25.5q0 21 20.5 33t40.5 15t41 3q34 0 70.5 -11t51.5 -34h3l30 58q-3 1 -21 8.5t-22.5 9t-19.5 7t-22 7t-20 4.5t-24 4t-23 1q-29 0 -56.5 -5t-54 -16.5 t-43 -34t-16.5 -53.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M863 504q0 112 -79.5 191.5t-191.5 79.5t-191 -79.5t-79 -191.5t79 -191t191 -79t191.5 79t79.5 191zM1726 505q0 112 -79 191t-191 79t-191.5 -79t-79.5 -191q0 -113 79.5 -192t191.5 -79t191 79.5t79 191.5zM2048 1314v-1348q0 -44 -31.5 -75.5t-76.5 -31.5h-1832 q-45 0 -76.5 31.5t-31.5 75.5v1348q0 44 31.5 75.5t76.5 31.5h431q44 0 76 -31.5t32 -75.5v-161h754v161q0 44 32 75.5t76 31.5h431q45 0 76.5 -31.5t31.5 -75.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1430 953zM1690 749q148 0 253 -98.5t105 -244.5q0 -157 -109 -261.5t-267 -104.5q-85 0 -162 27.5t-138 73.5t-118 106t-109 126.5t-103.5 132.5t-108.5 126t-117 106t-136 73.5t-159 27.5q-154 0 -251.5 -91.5t-97.5 -244.5q0 -157 104 -250t263 -93q100 0 208 37.5 t193 98.5q5 4 21 18.5t30 24t22 9.5q14 0 24.5 -10.5t10.5 -24.5q0 -24 -60 -77q-101 -88 -234.5 -142t-260.5 -54q-133 0 -245.5 58t-180 165t-67.5 241q0 205 141.5 341t347.5 136q120 0 226.5 -43.5t185.5 -113t151.5 -153t139 -167.5t133.5 -153.5t149.5 -113 t172.5 -43.5q102 0 168.5 61.5t66.5 162.5q0 95 -64.5 159t-159.5 64q-30 0 -81.5 -18.5t-68.5 -18.5q-20 0 -35.5 15t-15.5 35q0 18 8.5 57t8.5 59q0 159 -107.5 263t-266.5 104q-58 0 -111.5 -18.5t-84 -40.5t-55.5 -40.5t-33 -18.5q-15 0 -25.5 10.5t-10.5 25.5 q0 19 25 46q59 67 147 103.5t182 36.5q191 0 318 -125.5t127 -315.5q0 -37 -4 -66q57 15 115 15z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1216 832q0 26 -19 45t-45 19h-128v128q0 26 -19 45t-45 19t-45 -19t-19 -45v-128h-128q-26 0 -45 -19t-19 -45t19 -45t45 -19h128v-128q0 -26 19 -45t45 -19t45 19t19 45v128h128q26 0 45 19t19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M1280 832q0 26 -19 45t-45 19t-45 -19l-147 -146v293q0 26 -19 45t-45 19t-45 -19t-19 -45v-293l-147 146q-19 19 -45 19t-45 -19t-19 -45t19 -45l256 -256q19 -19 45 -19t45 19l256 256q19 19 19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M212 768l623 -665l-300 665h-323zM1024 -4l349 772h-698zM538 896l204 384h-262l-288 -384h346zM1213 103l623 665h-323zM683 896h682l-204 384h-274zM1510 896h346l-288 384h-262zM1651 1382l384 -512q14 -18 13 -41.5t-17 -40.5l-960 -1024q-18 -20 -47 -20t-47 20 l-960 1024q-16 17 -17 40.5t13 41.5l384 512q18 26 51 26h1152q33 0 51 -26z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1811 -19q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83 q19 19 45 19t45 -19l83 -83zM237 19q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -82l83 82q19 19 45 19t45 -19l83 -82l64 64v293l-210 314q-17 26 -7 56.5t40 40.5l177 58v299h128v128h256v128h256v-128h256v-128h128v-299l177 -58q30 -10 40 -40.5t-7 -56.5l-210 -314 v-293l19 18q19 19 45 19t45 -19l83 -82l83 82q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83zM640 1152v-128l384 128l384 -128v128h-128v128h-512v-128h-128z" />
|
||||
<glyph unicode="" d="M576 0l96 448l-96 128l-128 64zM832 0l128 640l-128 -64l-96 -128zM992 1010q-2 4 -4 6q-10 8 -96 8q-70 0 -167 -19q-7 -2 -21 -2t-21 2q-97 19 -167 19q-86 0 -96 -8q-2 -2 -4 -6q2 -18 4 -27q2 -3 7.5 -6.5t7.5 -10.5q2 -4 7.5 -20.5t7 -20.5t7.5 -17t8.5 -17t9 -14 t12 -13.5t14 -9.5t17.5 -8t20.5 -4t24.5 -2q36 0 59 12.5t32.5 30t14.5 34.5t11.5 29.5t17.5 12.5h12q11 0 17.5 -12.5t11.5 -29.5t14.5 -34.5t32.5 -30t59 -12.5q13 0 24.5 2t20.5 4t17.5 8t14 9.5t12 13.5t9 14t8.5 17t7.5 17t7 20.5t7.5 20.5q2 7 7.5 10.5t7.5 6.5 q2 9 4 27zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 61 4.5 118t19 125.5t37.5 123.5t63.5 103.5t93.5 74.5l-90 220h214q-22 64 -22 128q0 12 2 32q-194 40 -194 96q0 57 210 99q17 62 51.5 134t70.5 114q32 37 76 37q30 0 84 -31t84 -31t84 31 t84 31q44 0 76 -37q36 -42 70.5 -114t51.5 -134q210 -42 210 -99q0 -56 -194 -96q7 -81 -20 -160h214l-82 -225q63 -33 107.5 -96.5t65.5 -143.5t29 -151.5t8 -148.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M2301 500q12 -103 -22 -198.5t-99 -163.5t-158.5 -106t-196.5 -31q-161 11 -279.5 125t-134.5 274q-12 111 27.5 210.5t118.5 170.5l-71 107q-96 -80 -151 -194t-55 -244q0 -27 -18.5 -46.5t-45.5 -19.5h-256h-69q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5 t-131.5 316.5t131.5 316.5t316.5 131.5q76 0 152 -27l24 45q-123 110 -304 110h-64q-26 0 -45 19t-19 45t19 45t45 19h128q78 0 145 -13.5t116.5 -38.5t71.5 -39.5t51 -36.5h512h115l-85 128h-222q-30 0 -49 22.5t-14 52.5q4 23 23 38t43 15h253q33 0 53 -28l70 -105 l114 114q19 19 46 19h101q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-179l115 -172q131 63 275 36q143 -26 244 -134.5t118 -253.5zM448 128q115 0 203 72.5t111 183.5h-314q-35 0 -55 31q-18 32 -1 63l147 277q-47 13 -91 13q-132 0 -226 -94t-94 -226t94 -226 t226 -94zM1856 128q132 0 226 94t94 226t-94 226t-226 94q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94z" />
|
||||
<glyph unicode="" d="M1408 0q0 -63 -61.5 -113.5t-164 -81t-225 -46t-253.5 -15.5t-253.5 15.5t-225 46t-164 81t-61.5 113.5q0 49 33 88.5t91 66.5t118 44.5t131 29.5q26 5 48 -10.5t26 -41.5q5 -26 -10.5 -48t-41.5 -26q-58 -10 -106 -23.5t-76.5 -25.5t-48.5 -23.5t-27.5 -19.5t-8.5 -12 q3 -11 27 -26.5t73 -33t114 -32.5t160.5 -25t201.5 -10t201.5 10t160.5 25t114 33t73 33.5t27 27.5q-1 4 -8.5 11t-27.5 19t-48.5 23.5t-76.5 25t-106 23.5q-26 4 -41.5 26t-10.5 48q4 26 26 41.5t48 10.5q71 -12 131 -29.5t118 -44.5t91 -66.5t33 -88.5zM1024 896v-384 q0 -26 -19 -45t-45 -19h-64v-384q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v384h-64q-26 0 -45 19t-19 45v384q0 53 37.5 90.5t90.5 37.5h384q53 0 90.5 -37.5t37.5 -90.5zM928 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5 t158.5 -65.5t65.5 -158.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1280 512h305q-5 -6 -10 -10.5t-9 -7.5l-3 -4l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-5 2 -21 20h369q22 0 39.5 13.5t22.5 34.5l70 281l190 -667q6 -20 23 -33t39 -13q21 0 38 13t23 33l146 485l56 -112q18 -35 57 -35zM1792 940q0 -145 -103 -300h-369l-111 221 q-8 17 -25.5 27t-36.5 8q-45 -5 -56 -46l-129 -430l-196 686q-6 20 -23.5 33t-39.5 13t-39 -13.5t-22 -34.5l-116 -464h-423q-103 155 -103 300q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124 t127 -344z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292 q11 134 80.5 249t182 188t245.5 88q170 19 319 -54t236 -212t87 -306zM128 960q0 -185 131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5z" />
|
||||
<glyph unicode="" d="M1472 1408q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-382 -383q126 -156 126 -359q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123t223.5 45.5 q203 0 359 -126l382 382h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM576 0q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M830 1220q145 -72 233.5 -210.5t88.5 -305.5q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5 t-147.5 384.5q0 167 88.5 305.5t233.5 210.5q-165 96 -228 273q-6 16 3.5 29.5t26.5 13.5h69q21 0 29 -20q44 -106 140 -171t214 -65t214 65t140 171q8 20 37 20h61q17 0 26.5 -13.5t3.5 -29.5q-63 -177 -228 -273zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" d="M1024 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-149 16 -270.5 103t-186.5 223.5t-53 291.5q16 204 160 353.5t347 172.5q118 14 228 -19t198 -103l255 254h-134q-14 0 -23 9t-9 23v64zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5t-147.5 384.5q0 201 126 359l-52 53l-101 -111q-9 -10 -22 -10.5t-23 7.5l-48 44q-10 8 -10.5 21.5t8.5 23.5l105 115l-111 112v-134q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9 t-9 23v288q0 26 19 45t45 19h288q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-133l106 -107l86 94q9 10 22 10.5t23 -7.5l48 -44q10 -8 10.5 -21.5t-8.5 -23.5l-90 -99l57 -56q158 126 359 126t359 -126l255 254h-134q-14 0 -23 9t-9 23v64zM832 256q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1790 1007q12 -155 -52.5 -292t-186 -224t-271.5 -103v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-512v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23 t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292q17 206 164.5 356.5t352.5 169.5q206 21 377 -94q171 115 377 94q205 -19 352.5 -169.5t164.5 -356.5zM896 647q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM576 512q115 0 218 57q-154 165 -154 391 q0 224 154 391q-103 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5zM1152 128v260q-137 15 -256 94q-119 -79 -256 -94v-260h512zM1216 512q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5q-115 0 -218 -57q154 -167 154 -391 q0 -226 -154 -391q103 -57 218 -57z" />
|
||||
<glyph unicode="" horiz-adv-x="1920" d="M1536 1120q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-31 -182 -166 -312t-318 -156q-210 -29 -384.5 80t-241.5 300q-117 6 -221 57.5t-177.5 133t-113.5 192.5t-32 230 q9 135 78 252t182 191.5t248 89.5q118 14 227.5 -19t198.5 -103l255 254h-134q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q59 -74 93 -169q182 -9 328 -124l255 254h-134q-14 0 -23 9 t-9 23v64zM1024 704q0 20 -4 58q-162 -25 -271 -150t-109 -292q0 -20 4 -58q162 25 271 150t109 292zM128 704q0 -168 111 -294t276 -149q-3 29 -3 59q0 210 135 369.5t338 196.5q-53 120 -163.5 193t-245.5 73q-185 0 -316.5 -131.5t-131.5 -316.5zM1088 -128 q185 0 316.5 131.5t131.5 316.5q0 168 -111 294t-276 149q3 -29 3 -59q0 -210 -135 -369.5t-338 -196.5q53 -120 163.5 -193t245.5 -73z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1664 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-32 -180 -164.5 -310t-313.5 -157q-223 -34 -409 90q-117 -78 -256 -93v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23 t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-155 17 -279.5 109.5t-187 237.5t-39.5 307q25 187 159.5 322.5t320.5 164.5q224 34 410 -90q146 97 320 97q201 0 359 -126l255 254h-134q-14 0 -23 9 t-9 23v64zM896 391q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM128 704q0 -185 131.5 -316.5t316.5 -131.5q117 0 218 57q-154 167 -154 391t154 391q-101 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5zM1216 256q185 0 316.5 131.5t131.5 316.5 t-131.5 316.5t-316.5 131.5q-117 0 -218 -57q154 -167 154 -391t-154 -391q101 -57 218 -57z" />
|
||||
<glyph unicode="" d="M1472 1408q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-213 -214l140 -140q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-140 141l-78 -79q126 -156 126 -359q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5 t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123t223.5 45.5q203 0 359 -126l78 78l-172 172q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l172 -172l213 213h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM576 0q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M640 892q217 -24 364.5 -187.5t147.5 -384.5q0 -167 -87 -306t-236 -212t-319 -54q-133 15 -245.5 88t-182 188t-80.5 249q-12 155 52.5 292t186 224t271.5 103v132h-160q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h160v165l-92 -92q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22 t9 23l202 201q19 19 45 19t45 -19l202 -201q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-92 92v-165h160q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-160v-132zM576 -128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5 t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1901 621q19 -19 19 -45t-19 -45l-294 -294q-9 -10 -22.5 -10t-22.5 10l-45 45q-10 9 -10 22.5t10 22.5l185 185h-294v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-132q-24 -217 -187.5 -364.5t-384.5 -147.5q-167 0 -306 87t-212 236t-54 319q15 133 88 245.5 t188 182t249 80.5q155 12 292 -52.5t224 -186t103 -271.5h132v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224h294l-185 185q-10 9 -10 22.5t10 22.5l45 45q9 10 22.5 10t22.5 -10zM576 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5 t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-612q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v612q-217 24 -364.5 187.5t-147.5 384.5q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM576 512q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1024 576q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1152 576q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123 t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" d="M1451 1408q35 0 60 -25t25 -60v-1366q0 -35 -25 -60t-60 -25h-391v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-735q-35 0 -60 25t-25 60v1366q0 35 25 60t60 25h1366z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M0 939q0 108 37.5 203.5t103.5 166.5t152 123t185 78t202 26q158 0 294 -66.5t221 -193.5t85 -287q0 -96 -19 -188t-60 -177t-100 -149.5t-145 -103t-189 -38.5q-68 0 -135 32t-96 88q-10 -39 -28 -112.5t-23.5 -95t-20.5 -71t-26 -71t-32 -62.5t-46 -77.5t-62 -86.5 l-14 -5l-9 10q-15 157 -15 188q0 92 21.5 206.5t66.5 287.5t52 203q-32 65 -32 169q0 83 52 156t132 73q61 0 95 -40.5t34 -102.5q0 -66 -44 -191t-44 -187q0 -63 45 -104.5t109 -41.5q55 0 102 25t78.5 68t56 95t38 110.5t20 111t6.5 99.5q0 173 -109.5 269.5t-285.5 96.5 q-200 0 -334 -129.5t-134 -328.5q0 -44 12.5 -85t27 -65t27 -45.5t12.5 -30.5q0 -28 -15 -73t-37 -45q-2 0 -17 3q-51 15 -90.5 56t-61 94.5t-32.5 108t-11 106.5z" />
|
||||
<glyph unicode="" d="M985 562q13 0 97.5 -44t89.5 -53q2 -5 2 -15q0 -33 -17 -76q-16 -39 -71 -65.5t-102 -26.5q-57 0 -190 62q-98 45 -170 118t-148 185q-72 107 -71 194v8q3 91 74 158q24 22 52 22q6 0 18 -1.5t19 -1.5q19 0 26.5 -6.5t15.5 -27.5q8 -20 33 -88t25 -75q0 -21 -34.5 -57.5 t-34.5 -46.5q0 -7 5 -15q34 -73 102 -137q56 -53 151 -101q12 -7 22 -7q15 0 54 48.5t52 48.5zM782 32q127 0 243.5 50t200.5 134t134 200.5t50 243.5t-50 243.5t-134 200.5t-200.5 134t-243.5 50t-243.5 -50t-200.5 -134t-134 -200.5t-50 -243.5q0 -203 120 -368l-79 -233 l242 77q158 -104 345 -104zM782 1414q153 0 292.5 -60t240.5 -161t161 -240.5t60 -292.5t-60 -292.5t-161 -240.5t-240.5 -161t-292.5 -60q-195 0 -365 94l-417 -134l136 405q-108 178 -108 389q0 153 60 292.5t161 240.5t240.5 161t292.5 60z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M128 128h1024v128h-1024v-128zM128 640h1024v128h-1024v-128zM1696 192q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM128 1152h1024v128h-1024v-128zM1696 704q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1696 1216 q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1792 384v-384h-1792v384h1792zM1792 896v-384h-1792v384h1792zM1792 1408v-384h-1792v384h1792z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1664 512h352q13 0 22.5 -9.5t9.5 -22.5v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-352q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5 t-9.5 22.5v352h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v352q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5v-352zM928 288q0 -52 38 -90t90 -38h256v-238q-68 -50 -171 -50h-874q-121 0 -194 69t-73 190q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q79 -61 154.5 -91.5t164.5 -30.5t164.5 30.5t154.5 91.5q20 17 39 17q132 0 217 -96h-223q-52 0 -90 -38t-38 -90v-192z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1781 320l249 -249q9 -9 9 -23q0 -13 -9 -22l-136 -136q-9 -9 -22 -9q-14 0 -23 9l-249 249l-249 -249q-9 -9 -23 -9q-13 0 -22 9l-136 136 q-9 9 -9 22q0 14 9 23l249 249l-249 249q-9 9 -9 23q0 13 9 22l136 136q9 9 22 9q14 0 23 -9l249 -249l249 249q9 9 23 9q13 0 22 -9l136 -136q9 -9 9 -22q0 -14 -9 -23zM1283 320l-181 -181q-37 -37 -37 -91q0 -53 37 -90l83 -83q-21 -3 -44 -3h-874q-121 0 -194 69 t-73 190q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q154 -122 319 -122t319 122q20 17 39 17q28 0 57 -6q-28 -27 -41 -50t-13 -56q0 -54 37 -91z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M256 512h1728q26 0 45 -19t19 -45v-448h-256v256h-1536v-256h-256v1216q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-704zM832 832q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM2048 576v64q0 159 -112.5 271.5t-271.5 112.5h-704 q-26 0 -45 -19t-19 -45v-384h1152z" />
|
||||
<glyph unicode="" d="M1536 1536l-192 -448h192v-192h-274l-55 -128h329v-192h-411l-357 -832l-357 832h-411v192h329l-55 128h-274v192h192l-192 448h256l323 -768h378l323 768h256zM768 320l108 256h-216z" />
|
||||
<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM768 192q80 0 136 56t56 136t-56 136t-136 56 t-136 -56t-56 -136t56 -136t136 -56zM1344 768v512h-1152v-512h1152z" />
|
||||
<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM288 224q66 0 113 47t47 113t-47 113t-113 47 t-113 -47t-47 -113t47 -113t113 -47zM704 768v512h-544v-512h544zM1248 224q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM1408 768v512h-576v-512h576z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M597 1115v-1173q0 -25 -12.5 -42.5t-36.5 -17.5q-17 0 -33 8l-465 233q-21 10 -35.5 33.5t-14.5 46.5v1140q0 20 10 34t29 14q14 0 44 -15l511 -256q3 -3 3 -5zM661 1014l534 -866l-534 266v600zM1792 996v-1054q0 -25 -14 -40.5t-38 -15.5t-47 13l-441 220zM1789 1116 q0 -3 -256.5 -419.5t-300.5 -487.5l-390 634l324 527q17 28 52 28q14 0 26 -6l541 -270q4 -2 4 -6z" />
|
||||
<glyph unicode="" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1408v-1536h-1536v1536h1536z" />
|
||||
<glyph unicode="" horiz-adv-x="2296" d="M478 -139q-8 -16 -27 -34.5t-37 -25.5q-25 -9 -51.5 3.5t-28.5 31.5q-1 22 40 55t68 38q23 4 34 -21.5t2 -46.5zM1819 -139q7 -16 26 -34.5t38 -25.5q25 -9 51.5 3.5t27.5 31.5q2 22 -39.5 55t-68.5 38q-22 4 -33 -21.5t-2 -46.5zM1867 -30q13 -27 56.5 -59.5t77.5 -41.5 q45 -13 82 4.5t37 50.5q0 46 -67.5 100.5t-115.5 59.5q-40 5 -63.5 -37.5t-6.5 -76.5zM428 -30q-13 -27 -56 -59.5t-77 -41.5q-45 -13 -82 4.5t-37 50.5q0 46 67.5 100.5t115.5 59.5q40 5 63 -37.5t6 -76.5zM1158 1094h1q-41 0 -76 -15q27 -8 44 -30.5t17 -49.5 q0 -35 -27 -60t-65 -25q-52 0 -80 43q-5 -23 -5 -42q0 -74 56 -126.5t135 -52.5q80 0 136 52.5t56 126.5t-56 126.5t-136 52.5zM1462 1312q-99 109 -220.5 131.5t-245.5 -44.5q27 60 82.5 96.5t118 39.5t121.5 -17t99.5 -74.5t44.5 -131.5zM2212 73q8 -11 -11 -42 q7 -23 7 -40q1 -56 -44.5 -112.5t-109.5 -91.5t-118 -37q-48 -2 -92 21.5t-66 65.5q-687 -25 -1259 0q-23 -41 -66.5 -65t-92.5 -22q-86 3 -179.5 80.5t-92.5 160.5q2 22 7 40q-19 31 -11 42q6 10 31 1q14 22 41 51q-7 29 2 38q11 10 39 -4q29 20 59 34q0 29 13 37 q23 12 51 -16q35 5 61 -2q18 -4 38 -19v73q-11 0 -18 2q-53 10 -97 44.5t-55 87.5q-9 38 0 81q15 62 93 95q2 17 19 35.5t36 23.5t33 -7.5t19 -30.5h13q46 -5 60 -23q3 -3 5 -7q10 1 30.5 3.5t30.5 3.5q-15 11 -30 17q-23 40 -91 43q0 6 1 10q-62 2 -118.5 18.5t-84.5 47.5 q-32 36 -42.5 92t-2.5 112q16 126 90 179q23 16 52 4.5t32 -40.5q0 -1 1.5 -14t2.5 -21t3 -20t5.5 -19t8.5 -10q27 -14 76 -12q48 46 98 74q-40 4 -162 -14l47 46q61 58 163 111q145 73 282 86q-20 8 -41 15.5t-47 14t-42.5 10.5t-47.5 11t-43 10q595 126 904 -139 q98 -84 158 -222q85 -10 121 9h1q5 3 8.5 10t5.5 19t3 19.5t3 21.5l1 14q3 28 32 40t52 -5q73 -52 91 -178q7 -57 -3.5 -113t-42.5 -91q-28 -32 -83.5 -48.5t-115.5 -18.5v-10q-71 -2 -95 -43q-14 -5 -31 -17q11 -1 32 -3.5t30 -3.5q1 4 5 8q16 18 60 23h13q5 18 19 30t33 8 t36 -23t19 -36q79 -32 93 -95q9 -40 1 -81q-12 -53 -56 -88t-97 -44q-10 -2 -17 -2q0 -49 -1 -73q20 15 38 19q26 7 61 2q28 28 51 16q14 -9 14 -37q33 -16 59 -34q27 13 38 4q10 -10 2 -38q28 -30 41 -51q23 8 31 -1zM1937 1025q0 -29 -9 -54q82 -32 112 -132 q4 37 -9.5 98.5t-41.5 90.5q-20 19 -36 17t-16 -20zM1859 925q35 -42 47.5 -108.5t-0.5 -124.5q67 13 97 45q13 14 18 28q-3 64 -31 114.5t-79 66.5q-15 -15 -52 -21zM1822 921q-30 0 -44 1q42 -115 53 -239q21 0 43 3q16 68 1 135t-53 100zM258 839q30 100 112 132 q-9 25 -9 54q0 18 -16.5 20t-35.5 -17q-28 -29 -41.5 -90.5t-9.5 -98.5zM294 737q29 -31 97 -45q-13 58 -0.5 124.5t47.5 108.5v0q-37 6 -52 21q-51 -16 -78.5 -66t-31.5 -115q9 -17 18 -28zM471 683q14 124 73 235q-19 -4 -55 -18l-45 -19v1q-46 -89 -20 -196q25 -3 47 -3z M1434 644q8 -38 16.5 -108.5t11.5 -89.5q3 -18 9.5 -21.5t23.5 4.5q40 20 62 85.5t23 125.5q-24 2 -146 4zM1152 1285q-116 0 -199 -82.5t-83 -198.5q0 -117 83 -199.5t199 -82.5t199 82.5t83 199.5q0 116 -83 198.5t-199 82.5zM1380 646q-106 2 -211 0v1q-1 -27 2.5 -86 t13.5 -66q29 -14 93.5 -14.5t95.5 10.5q9 3 11 39t-0.5 69.5t-4.5 46.5zM1112 447q8 4 9.5 48t-0.5 88t-4 63v1q-212 -3 -214 -3q-4 -20 -7 -62t0 -83t14 -46q34 -15 101 -16t101 10zM718 636q-16 -59 4.5 -118.5t77.5 -84.5q15 -8 24 -5t12 21q3 16 8 90t10 103 q-69 -2 -136 -6zM591 510q3 -23 -34 -36q132 -141 271.5 -240t305.5 -154q172 49 310.5 146t293.5 250q-33 13 -30 34l3 9v1v-1q-17 2 -50 5.5t-48 4.5q-26 -90 -82 -132q-51 -38 -82 1q-5 6 -9 14q-7 13 -17 62q-2 -5 -5 -9t-7.5 -7t-8 -5.5t-9.5 -4l-10 -2.5t-12 -2 l-12 -1.5t-13.5 -1t-13.5 -0.5q-106 -9 -163 11q-4 -17 -10 -26.5t-21 -15t-23 -7t-36 -3.5q-2 0 -3 -0.5t-3 -0.5h-3q-179 -17 -203 40q-2 -63 -56 -54q-47 8 -91 54q-12 13 -20 26q-17 29 -26 65q-58 -6 -87 -10q1 -2 4 -10zM507 -118q3 14 3 30q-17 71 -51 130t-73 70 q-41 12 -101.5 -14.5t-104.5 -80t-39 -107.5q35 -53 100 -93t119 -42q51 -2 94 28t53 79zM510 53q23 -63 27 -119q195 113 392 174q-98 52 -180.5 120t-179.5 165q-6 -4 -29 -13q0 -2 -1 -5t-1 -4q31 -18 22 -37q-12 -23 -56 -34q-10 -13 -29 -24h-1q-2 -83 1 -150 q19 -34 35 -73zM579 -113q532 -21 1145 0q-254 147 -428 196q-76 -35 -156 -57q-8 -3 -16 0q-65 21 -129 49q-208 -60 -416 -188h-1v-1q1 0 1 1zM1763 -67q4 54 28 120q14 38 33 71l-1 -1q3 77 3 153q-15 8 -30 25q-42 9 -56 33q-9 20 22 38q-2 4 -2 9q-16 4 -28 12 q-204 -190 -383 -284q198 -59 414 -176zM2155 -90q5 54 -39 107.5t-104 80t-102 14.5q-38 -11 -72.5 -70.5t-51.5 -129.5q0 -16 3 -30q10 -49 53 -79t94 -28q54 2 119 42t100 93z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1524 -25q0 -68 -48 -116t-116 -48t-116.5 48t-48.5 116t48.5 116.5t116.5 48.5t116 -48.5t48 -116.5zM775 -25q0 -68 -48.5 -116t-116.5 -48t-116 48t-48 116t48 116.5t116 48.5t116.5 -48.5t48.5 -116.5zM0 1469q57 -60 110.5 -104.5t121 -82t136 -63t166 -45.5 t200 -31.5t250 -18.5t304 -9.5t372.5 -2.5q139 0 244.5 -5t181 -16.5t124 -27.5t71 -39.5t24 -51.5t-19.5 -64t-56.5 -76.5t-89.5 -91t-116 -104.5t-139 -119q-185 -157 -286 -247q29 51 76.5 109t94 105.5t94.5 98.5t83 91.5t54 80.5t13 70t-45.5 55.5t-116.5 41t-204 23.5 t-304 5q-168 -2 -314 6t-256 23t-204.5 41t-159.5 51.5t-122.5 62.5t-91.5 66.5t-68 71.5t-50.5 69.5t-40 68t-36.5 59.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M896 1472q-169 0 -323 -66t-265.5 -177.5t-177.5 -265.5t-66 -323t66 -323t177.5 -265.5t265.5 -177.5t323 -66t323 66t265.5 177.5t177.5 265.5t66 323t-66 323t-177.5 265.5t-265.5 177.5t-323 66zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348 t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM496 704q16 0 16 -16v-480q0 -16 -16 -16h-32q-16 0 -16 16v480q0 16 16 16h32zM896 640q53 0 90.5 -37.5t37.5 -90.5q0 -35 -17.5 -64t-46.5 -46v-114q0 -14 -9 -23 t-23 -9h-64q-14 0 -23 9t-9 23v114q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5zM896 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM544 928v-96 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v96q0 93 65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5v-96q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v96q0 146 -103 249t-249 103t-249 -103t-103 -249zM1408 192v512q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-512 q0 -26 19 -45t45 -19h896q26 0 45 19t19 45z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1920 1024v-768h-1664v768h1664zM2048 448h128v384h-128v288q0 14 -9 23t-23 9h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288zM2304 832v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113 v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160q53 0 90.5 -37.5t37.5 -90.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M256 256v768h1280v-768h-1280zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M256 256v768h896v-768h-896zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M256 256v768h512v-768h-512zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9h-1856q-14 0 -23 -9t-9 -23 v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1133 493q31 -30 14 -69q-17 -40 -59 -40h-382l201 -476q10 -25 0 -49t-34 -35l-177 -75q-25 -10 -49 0t-35 34l-191 452l-312 -312q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v1504q0 42 40 59q12 5 24 5q27 0 45 -19z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M832 1408q-320 0 -320 -224v-416h128v-128h-128v-544q0 -224 320 -224h64v-128h-64q-272 0 -384 146q-112 -146 -384 -146h-64v128h64q320 0 320 224v544h-128v128h128v416q0 224 -320 224h-64v128h64q272 0 384 -146q112 146 384 146h64v-128h-64z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M2048 1152h-128v-1024h128v-384h-384v128h-1280v-128h-384v384h128v1024h-128v384h384v-128h1280v128h384v-384zM1792 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 -128v128h-128v-128h128zM1664 0v128h128v1024h-128v128h-1280v-128h-128v-1024h128v-128 h1280zM1920 -128v128h-128v-128h128zM1280 896h384v-768h-896v256h-384v768h896v-256zM512 512h640v512h-640v-512zM1536 256v512h-256v-384h-384v-128h640z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M2304 768h-128v-640h128v-384h-384v128h-896v-128h-384v384h128v128h-384v-128h-384v384h128v640h-128v384h384v-128h896v128h384v-384h-128v-128h384v128h384v-384zM2048 1024v-128h128v128h-128zM1408 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 256 v128h-128v-128h128zM1536 384h-128v-128h128v128zM384 384h896v128h128v640h-128v128h-896v-128h-128v-640h128v-128zM896 -128v128h-128v-128h128zM2176 -128v128h-128v-128h128zM2048 128v640h-128v128h-384v-384h128v-384h-384v128h-384v-128h128v-128h896v128h128z" />
|
||||
<glyph unicode="" d="M1024 288v-416h-928q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1344q40 0 68 -28t28 -68v-928h-416q-40 0 -68 -28t-28 -68zM1152 256h381q-15 -82 -65 -132l-184 -184q-50 -50 -132 -65v381z" />
|
||||
<glyph unicode="" d="M1400 256h-248v-248q29 10 41 22l185 185q12 12 22 41zM1120 384h288v896h-1280v-1280h896v288q0 40 28 68t68 28zM1536 1312v-1024q0 -40 -20 -88t-48 -76l-184 -184q-28 -28 -76 -48t-88 -20h-1024q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1344q40 0 68 -28t28 -68 z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1951 538q0 -26 -15.5 -44.5t-38.5 -23.5q-8 -2 -18 -2h-153v140h153q10 0 18 -2q23 -5 38.5 -23.5t15.5 -44.5zM1933 751q0 -25 -15 -42t-38 -21q-3 -1 -15 -1h-139v129h139q3 0 8.5 -0.5t6.5 -0.5q23 -4 38 -21.5t15 -42.5zM728 587v308h-228v-308q0 -58 -38 -94.5 t-105 -36.5q-108 0 -229 59v-112q53 -15 121 -23t109 -9l42 -1q328 0 328 217zM1442 403v113q-99 -52 -200 -59q-108 -8 -169 41t-61 142t61 142t169 41q101 -7 200 -58v112q-48 12 -100 19.5t-80 9.5l-28 2q-127 6 -218.5 -14t-140.5 -60t-71 -88t-22 -106t22 -106t71 -88 t140.5 -60t218.5 -14q101 4 208 31zM2176 518q0 54 -43 88.5t-109 39.5v3q57 8 89 41.5t32 79.5q0 55 -41 88t-107 36q-3 0 -12 0.5t-14 0.5h-455v-510h491q74 0 121.5 36.5t47.5 96.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90 t90 38h2048q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M858 295v693q-106 -41 -172 -135.5t-66 -211.5t66 -211.5t172 -134.5zM1362 641q0 117 -66 211.5t-172 135.5v-694q106 41 172 135.5t66 211.5zM1577 641q0 -159 -78.5 -294t-213.5 -213.5t-294 -78.5q-119 0 -227.5 46.5t-187 125t-125 187t-46.5 227.5q0 159 78.5 294 t213.5 213.5t294 78.5t294 -78.5t213.5 -213.5t78.5 -294zM1960 634q0 139 -55.5 261.5t-147.5 205.5t-213.5 131t-252.5 48h-301q-176 0 -323.5 -81t-235 -230t-87.5 -335q0 -171 87 -317.5t236 -231.5t323 -85h301q129 0 251.5 50.5t214.5 135t147.5 202.5t55.5 246z M2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1664 -96v1088q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5v-1088q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5zM1792 992v-1088q0 -66 -47 -113t-113 -47h-1088q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1088q66 0 113 -47t47 -113 zM1408 1376v-160h-128v160q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5v-1088q0 -13 9.5 -22.5t22.5 -9.5h160v-128h-160q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1088q66 0 113 -47t47 -113z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1728 1088l-384 -704h768zM448 1088l-384 -704h768zM1269 1280q-14 -40 -45.5 -71.5t-71.5 -45.5v-1291h608q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1344q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h608v1291q-40 14 -71.5 45.5t-45.5 71.5h-491q-14 0 -23 9t-9 23v64 q0 14 9 23t23 9h491q21 57 70 92.5t111 35.5t111 -35.5t70 -92.5h491q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-491zM1088 1264q33 0 56.5 23.5t23.5 56.5t-23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5zM2176 384q0 -73 -46.5 -131t-117.5 -91 t-144.5 -49.5t-139.5 -16.5t-139.5 16.5t-144.5 49.5t-117.5 91t-46.5 131q0 11 35 81t92 174.5t107 195.5t102 184t56 100q18 33 56 33t56 -33q4 -7 56 -100t102 -184t107 -195.5t92 -174.5t35 -81zM896 384q0 -73 -46.5 -131t-117.5 -91t-144.5 -49.5t-139.5 -16.5 t-139.5 16.5t-144.5 49.5t-117.5 91t-46.5 131q0 11 35 81t92 174.5t107 195.5t102 184t56 100q18 33 56 33t56 -33q4 -7 56 -100t102 -184t107 -195.5t92 -174.5t35 -81z" />
|
||||
<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM874 700q77 29 149 92.5t129.5 152.5t92.5 210t35 253h-1024q0 -132 35 -253t92.5 -210t129.5 -152.5t149 -92.5q19 -7 30.5 -23.5t11.5 -36.5t-11.5 -36.5t-30.5 -23.5q-77 -29 -149 -92.5 t-129.5 -152.5t-92.5 -210t-35 -253h1024q0 132 -35 253t-92.5 210t-129.5 152.5t-149 92.5q-19 7 -30.5 23.5t-11.5 36.5t11.5 36.5t30.5 23.5z" />
|
||||
<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM1280 1408h-1024q0 -66 9 -128h1006q9 61 9 128zM1280 -128q0 130 -34 249.5t-90.5 208t-126.5 152t-146 94.5h-230q-76 -31 -146 -94.5t-126.5 -152t-90.5 -208t-34 -249.5h1024z" />
|
||||
<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM1280 1408h-1024q0 -206 85 -384h854q85 178 85 384zM1223 192q-54 141 -145.5 241.5t-194.5 142.5h-230q-103 -42 -194.5 -142.5t-145.5 -241.5h910z" />
|
||||
<glyph unicode="" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM874 700q77 29 149 92.5t129.5 152.5t92.5 210t35 253h-1024q0 -132 35 -253t92.5 -210t129.5 -152.5t149 -92.5q19 -7 30.5 -23.5t11.5 -36.5t-11.5 -36.5t-30.5 -23.5q-137 -51 -244 -196 h700q-107 145 -244 196q-19 7 -30.5 23.5t-11.5 36.5t11.5 36.5t30.5 23.5z" />
|
||||
<glyph unicode="" d="M1504 -64q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v128q0 14 9 23t23 9h1472zM130 0q3 55 16 107t30 95t46 87t53.5 76t64.5 69.5t66 60t70.5 55t66.5 47.5t65 43q-43 28 -65 43t-66.5 47.5t-70.5 55t-66 60t-64.5 69.5t-53.5 76t-46 87 t-30 95t-16 107h1276q-3 -55 -16 -107t-30 -95t-46 -87t-53.5 -76t-64.5 -69.5t-66 -60t-70.5 -55t-66.5 -47.5t-65 -43q43 -28 65 -43t66.5 -47.5t70.5 -55t66 -60t64.5 -69.5t53.5 -76t46 -87t30 -95t16 -107h-1276zM1504 1536q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9 h-1472q-14 0 -23 9t-9 23v128q0 14 9 23t23 9h1472z" />
|
||||
<glyph unicode="" d="M768 1152q-53 0 -90.5 -37.5t-37.5 -90.5v-128h-32v93q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-429l-32 30v172q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-224q0 -47 35 -82l310 -296q39 -39 39 -102q0 -26 19 -45t45 -19h640q26 0 45 19t19 45v25 q0 41 10 77l108 436q10 36 10 77v246q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-32h-32v125q0 40 -25 72.5t-64 40.5q-14 2 -23 2q-46 0 -79 -33t-33 -79v-128h-32v122q0 51 -32.5 89.5t-82.5 43.5q-5 1 -13 1zM768 1280q84 0 149 -50q57 34 123 34q59 0 111 -27 t86 -76q27 7 59 7q100 0 170 -71.5t70 -171.5v-246q0 -51 -13 -108l-109 -436q-6 -24 -6 -71q0 -80 -56 -136t-136 -56h-640q-84 0 -138 58.5t-54 142.5l-308 296q-76 73 -76 175v224q0 99 70.5 169.5t169.5 70.5q11 0 16 -1q6 95 75.5 160t164.5 65q52 0 98 -21 q72 69 174 69z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M880 1408q-46 0 -79 -33t-33 -79v-656h-32v528q0 46 -33 79t-79 33t-79 -33t-33 -79v-528v-256l-154 205q-38 51 -102 51q-53 0 -90.5 -37.5t-37.5 -90.5q0 -43 26 -77l384 -512q38 -51 102 -51h688q34 0 61 22t34 56l76 405q5 32 5 59v498q0 46 -33 79t-79 33t-79 -33 t-33 -79v-272h-32v528q0 46 -33 79t-79 33t-79 -33t-33 -79v-528h-32v656q0 46 -33 79t-79 33zM880 1536q68 0 125.5 -35.5t88.5 -96.5q19 4 42 4q99 0 169.5 -70.5t70.5 -169.5v-17q105 6 180.5 -64t75.5 -175v-498q0 -40 -8 -83l-76 -404q-14 -79 -76.5 -131t-143.5 -52 h-688q-60 0 -114.5 27.5t-90.5 74.5l-384 512q-51 68 -51 154q0 106 75 181t181 75q78 0 128 -34v434q0 99 70.5 169.5t169.5 70.5q23 0 42 -4q31 61 88.5 96.5t125.5 35.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1073 -128h-177q-163 0 -226 141q-23 49 -23 102v5q-62 30 -98.5 88.5t-36.5 127.5q0 38 5 48h-261q-106 0 -181 75t-75 181t75 181t181 75h113l-44 17q-74 28 -119.5 93.5t-45.5 145.5q0 106 75 181t181 75q46 0 91 -17l628 -239h401q106 0 181 -75t75 -181v-668 q0 -88 -54 -157.5t-140 -90.5l-339 -85q-92 -23 -186 -23zM1024 583l-155 -71l-163 -74q-30 -14 -48 -41.5t-18 -60.5q0 -46 33 -79t79 -33q26 0 46 10l338 154q-49 10 -80.5 50t-31.5 90v55zM1344 272q0 46 -33 79t-79 33q-26 0 -46 -10l-290 -132q-28 -13 -37 -17 t-30.5 -17t-29.5 -23.5t-16 -29t-8 -40.5q0 -50 31.5 -82t81.5 -32q20 0 38 9l352 160q30 14 48 41.5t18 60.5zM1112 1024l-650 248q-24 8 -46 8q-53 0 -90.5 -37.5t-37.5 -90.5q0 -40 22.5 -73t59.5 -47l526 -200v-64h-640q-53 0 -90.5 -37.5t-37.5 -90.5t37.5 -90.5 t90.5 -37.5h535l233 106v198q0 63 46 106l111 102h-69zM1073 0q82 0 155 19l339 85q43 11 70 45.5t27 78.5v668q0 53 -37.5 90.5t-90.5 37.5h-308l-136 -126q-36 -33 -36 -82v-296q0 -46 33 -77t79 -31t79 35t33 81v208h32v-208q0 -70 -57 -114q52 -8 86.5 -48.5t34.5 -93.5 q0 -42 -23 -78t-61 -53l-310 -141h91z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1151 1536q61 0 116 -28t91 -77l572 -781q118 -159 118 -359v-355q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v177l-286 143h-546q-80 0 -136 56t-56 136v32q0 119 84.5 203.5t203.5 84.5h420l42 128h-686q-100 0 -173.5 67.5t-81.5 166.5q-65 79 -65 182v32 q0 80 56 136t136 56h959zM1920 -64v355q0 157 -93 284l-573 781q-39 52 -103 52h-959q-26 0 -45 -19t-19 -45q0 -32 1.5 -49.5t9.5 -40.5t25 -43q10 31 35.5 50t56.5 19h832v-32h-832q-26 0 -45 -19t-19 -45q0 -44 3 -58q8 -44 44 -73t81 -29h640h91q40 0 68 -28t28 -68 q0 -15 -5 -30l-64 -192q-10 -29 -35 -47.5t-56 -18.5h-443q-66 0 -113 -47t-47 -113v-32q0 -26 19 -45t45 -19h561q16 0 29 -7l317 -158q24 -13 38.5 -36t14.5 -50v-197q0 -26 19 -45t45 -19h384q26 0 45 19t19 45z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M816 1408q-48 0 -79.5 -34t-31.5 -82q0 -14 3 -28l150 -624h-26l-116 482q-9 38 -39.5 62t-69.5 24q-47 0 -79 -34t-32 -81q0 -11 4 -29q3 -13 39 -161t68 -282t32 -138v-227l-307 230q-34 26 -77 26q-52 0 -89.5 -36.5t-37.5 -88.5q0 -67 56 -110l507 -379 q34 -26 76 -26h694q33 0 59 20.5t34 52.5l100 401q8 30 10 88t9 86l116 478q3 12 3 26q0 46 -33 79t-80 33q-38 0 -69 -25.5t-40 -62.5l-99 -408h-26l132 547q3 14 3 28q0 47 -32 80t-80 33q-38 0 -68.5 -24t-39.5 -62l-145 -602h-127l-164 682q-9 38 -39.5 62t-68.5 24z M1461 -256h-694q-85 0 -153 51l-507 380q-50 38 -78.5 94t-28.5 118q0 105 75 179t180 74q25 0 49.5 -5.5t41.5 -11t41 -20.5t35 -23t38.5 -29.5t37.5 -28.5l-123 512q-7 35 -7 59q0 93 60 162t152 79q14 87 80.5 144.5t155.5 57.5q83 0 148 -51.5t85 -132.5l103 -428 l83 348q20 81 85 132.5t148 51.5q87 0 152.5 -54t82.5 -139q93 -10 155 -78t62 -161q0 -30 -7 -57l-116 -477q-5 -22 -5 -67q0 -51 -13 -108l-101 -401q-19 -75 -79.5 -122.5t-137.5 -47.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M640 1408q-53 0 -90.5 -37.5t-37.5 -90.5v-512v-384l-151 202q-41 54 -107 54q-52 0 -89 -38t-37 -90q0 -43 26 -77l384 -512q38 -51 102 -51h718q22 0 39.5 13.5t22.5 34.5l92 368q24 96 24 194v217q0 41 -28 71t-68 30t-68 -28t-28 -68h-32v61q0 48 -32 81.5t-80 33.5 q-46 0 -79 -33t-33 -79v-64h-32v90q0 55 -37 94.5t-91 39.5q-53 0 -90.5 -37.5t-37.5 -90.5v-96h-32v570q0 55 -37 94.5t-91 39.5zM640 1536q107 0 181.5 -77.5t74.5 -184.5v-220q22 2 32 2q99 0 173 -69q47 21 99 21q113 0 184 -87q27 7 56 7q94 0 159 -67.5t65 -161.5 v-217q0 -116 -28 -225l-92 -368q-16 -64 -68 -104.5t-118 -40.5h-718q-60 0 -114.5 27.5t-90.5 74.5l-384 512q-51 68 -51 154q0 105 74.5 180.5t179.5 75.5q71 0 130 -35v547q0 106 75 181t181 75zM768 128v384h-32v-384h32zM1024 128v384h-32v-384h32zM1280 128v384h-32 v-384h32z" />
|
||||
<glyph unicode="" d="M1288 889q60 0 107 -23q141 -63 141 -226v-177q0 -94 -23 -186l-85 -339q-21 -86 -90.5 -140t-157.5 -54h-668q-106 0 -181 75t-75 181v401l-239 628q-17 45 -17 91q0 106 75 181t181 75q80 0 145.5 -45.5t93.5 -119.5l17 -44v113q0 106 75 181t181 75t181 -75t75 -181 v-261q27 5 48 5q69 0 127.5 -36.5t88.5 -98.5zM1072 896q-33 0 -60.5 -18t-41.5 -48l-74 -163l-71 -155h55q50 0 90 -31.5t50 -80.5l154 338q10 20 10 46q0 46 -33 79t-79 33zM1293 761q-22 0 -40.5 -8t-29 -16t-23.5 -29.5t-17 -30.5t-17 -37l-132 -290q-10 -20 -10 -46 q0 -46 33 -79t79 -33q33 0 60.5 18t41.5 48l160 352q9 18 9 38q0 50 -32 81.5t-82 31.5zM128 1120q0 -22 8 -46l248 -650v-69l102 111q43 46 106 46h198l106 233v535q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5v-640h-64l-200 526q-14 37 -47 59.5t-73 22.5 q-53 0 -90.5 -37.5t-37.5 -90.5zM1180 -128q44 0 78.5 27t45.5 70l85 339q19 73 19 155v91l-141 -310q-17 -38 -53 -61t-78 -23q-53 0 -93.5 34.5t-48.5 86.5q-44 -57 -114 -57h-208v32h208q46 0 81 33t35 79t-31 79t-77 33h-296q-49 0 -82 -36l-126 -136v-308 q0 -53 37.5 -90.5t90.5 -37.5h668z" />
|
||||
<glyph unicode="" horiz-adv-x="1973" d="M857 992v-117q0 -13 -9.5 -22t-22.5 -9h-298v-812q0 -13 -9 -22.5t-22 -9.5h-135q-13 0 -22.5 9t-9.5 23v812h-297q-13 0 -22.5 9t-9.5 22v117q0 14 9 23t23 9h793q13 0 22.5 -9.5t9.5 -22.5zM1895 995l77 -961q1 -13 -8 -24q-10 -10 -23 -10h-134q-12 0 -21 8.5 t-10 20.5l-46 588l-189 -425q-8 -19 -29 -19h-120q-20 0 -29 19l-188 427l-45 -590q-1 -12 -10 -20.5t-21 -8.5h-135q-13 0 -23 10q-9 10 -9 24l78 961q1 12 10 20.5t21 8.5h142q20 0 29 -19l220 -520q10 -24 20 -51q3 7 9.5 24.5t10.5 26.5l221 520q9 19 29 19h141 q13 0 22 -8.5t10 -20.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1042 833q0 88 -60 121q-33 18 -117 18h-123v-281h162q66 0 102 37t36 105zM1094 548l205 -373q8 -17 -1 -31q-8 -16 -27 -16h-152q-20 0 -28 17l-194 365h-155v-350q0 -14 -9 -23t-23 -9h-134q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h294q128 0 190 -24q85 -31 134 -109 t49 -180q0 -92 -42.5 -165.5t-115.5 -109.5q6 -10 9 -16zM896 1376q-150 0 -286 -58.5t-234.5 -157t-157 -234.5t-58.5 -286t58.5 -286t157 -234.5t234.5 -157t286 -58.5t286 58.5t234.5 157t157 234.5t58.5 286t-58.5 286t-157 234.5t-234.5 157t-286 58.5zM1792 640 q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M605 303q153 0 257 104q14 18 3 36l-45 82q-6 13 -24 17q-16 2 -27 -11l-4 -3q-4 -4 -11.5 -10t-17.5 -13t-23.5 -14.5t-28.5 -13.5t-33.5 -9.5t-37.5 -3.5q-76 0 -125 50t-49 127q0 76 48 125.5t122 49.5q37 0 71.5 -14t50.5 -28l16 -14q11 -11 26 -10q16 2 24 14l53 78 q13 20 -2 39q-3 4 -11 12t-30 23.5t-48.5 28t-67.5 22.5t-86 10q-148 0 -246 -96.5t-98 -240.5q0 -146 97 -241.5t247 -95.5zM1235 303q153 0 257 104q14 18 4 36l-45 82q-8 14 -25 17q-16 2 -27 -11l-4 -3q-4 -4 -11.5 -10t-17.5 -13t-23.5 -14.5t-28.5 -13.5t-33.5 -9.5 t-37.5 -3.5q-76 0 -125 50t-49 127q0 76 48 125.5t122 49.5q37 0 71.5 -14t50.5 -28l16 -14q11 -11 26 -10q16 2 24 14l53 78q13 20 -2 39q-3 4 -11 12t-30 23.5t-48.5 28t-67.5 22.5t-86 10q-147 0 -245.5 -96.5t-98.5 -240.5q0 -146 97 -241.5t247 -95.5zM896 1376 q-150 0 -286 -58.5t-234.5 -157t-157 -234.5t-58.5 -286t58.5 -286t157 -234.5t234.5 -157t286 -58.5t286 58.5t234.5 157t157 234.5t58.5 286t-58.5 286t-157 234.5t-234.5 157t-286 58.5zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191 t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M736 736l384 -384l-384 -384l-672 672l672 672l168 -168l-96 -96l-72 72l-480 -480l480 -480l193 193l-289 287zM1312 1312l672 -672l-672 -672l-168 168l96 96l72 -72l480 480l-480 480l-193 -193l289 -287l-96 -96l-384 384z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M717 182l271 271l-279 279l-88 -88l192 -191l-96 -96l-279 279l279 279l40 -40l87 87l-127 128l-454 -454zM1075 190l454 454l-454 454l-271 -271l279 -279l88 88l-192 191l96 96l279 -279l-279 -279l-40 40l-87 -88zM1792 640q0 -182 -71 -348t-191 -286t-286 -191 t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M651 539q0 -39 -27.5 -66.5t-65.5 -27.5q-39 0 -66.5 27.5t-27.5 66.5q0 38 27.5 65.5t66.5 27.5q38 0 65.5 -27.5t27.5 -65.5zM1805 540q0 -39 -27.5 -66.5t-66.5 -27.5t-66.5 27.5t-27.5 66.5t27.5 66t66.5 27t66.5 -27t27.5 -66zM765 539q0 79 -56.5 136t-136.5 57 t-136.5 -56.5t-56.5 -136.5t56.5 -136.5t136.5 -56.5t136.5 56.5t56.5 136.5zM1918 540q0 80 -56.5 136.5t-136.5 56.5q-79 0 -136 -56.5t-57 -136.5t56.5 -136.5t136.5 -56.5t136.5 56.5t56.5 136.5zM850 539q0 -116 -81.5 -197.5t-196.5 -81.5q-116 0 -197.5 82t-81.5 197 t82 196.5t197 81.5t196.5 -81.5t81.5 -196.5zM2004 540q0 -115 -81.5 -196.5t-197.5 -81.5q-115 0 -196.5 81.5t-81.5 196.5t81.5 196.5t196.5 81.5q116 0 197.5 -81.5t81.5 -196.5zM1040 537q0 191 -135.5 326.5t-326.5 135.5q-125 0 -231 -62t-168 -168.5t-62 -231.5 t62 -231.5t168 -168.5t231 -62q191 0 326.5 135.5t135.5 326.5zM1708 1110q-254 111 -556 111q-319 0 -573 -110q117 0 223 -45.5t182.5 -122.5t122 -183t45.5 -223q0 115 43.5 219.5t118 180.5t177.5 123t217 50zM2187 537q0 191 -135 326.5t-326 135.5t-326.5 -135.5 t-135.5 -326.5t135.5 -326.5t326.5 -135.5t326 135.5t135 326.5zM1921 1103h383q-44 -51 -75 -114.5t-40 -114.5q110 -151 110 -337q0 -156 -77 -288t-209 -208.5t-287 -76.5q-133 0 -249 56t-196 155q-47 -56 -129 -179q-11 22 -53.5 82.5t-74.5 97.5 q-80 -99 -196.5 -155.5t-249.5 -56.5q-155 0 -287 76.5t-209 208.5t-77 288q0 186 110 337q-9 51 -40 114.5t-75 114.5h365q149 100 355 156.5t432 56.5q224 0 421 -56t348 -157z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M640 629q-188 0 -321 133t-133 320q0 188 133 321t321 133t321 -133t133 -321q0 -187 -133 -320t-321 -133zM640 1306q-92 0 -157.5 -65.5t-65.5 -158.5q0 -92 65.5 -157.5t157.5 -65.5t157.5 65.5t65.5 157.5q0 93 -65.5 158.5t-157.5 65.5zM1163 574q13 -27 15 -49.5 t-4.5 -40.5t-26.5 -38.5t-42.5 -37t-61.5 -41.5q-115 -73 -315 -94l73 -72l267 -267q30 -31 30 -74t-30 -73l-12 -13q-31 -30 -74 -30t-74 30q-67 68 -267 268l-267 -268q-31 -30 -74 -30t-73 30l-12 13q-31 30 -31 73t31 74l267 267l72 72q-203 21 -317 94 q-39 25 -61.5 41.5t-42.5 37t-26.5 38.5t-4.5 40.5t15 49.5q10 20 28 35t42 22t56 -2t65 -35q5 -4 15 -11t43 -24.5t69 -30.5t92 -24t113 -11q91 0 174 25.5t120 50.5l38 25q33 26 65 35t56 2t42 -22t28 -35z" />
|
||||
<glyph unicode="" d="M927 956q0 -66 -46.5 -112.5t-112.5 -46.5t-112.5 46.5t-46.5 112.5t46.5 112.5t112.5 46.5t112.5 -46.5t46.5 -112.5zM1141 593q-10 20 -28 32t-47.5 9.5t-60.5 -27.5q-10 -8 -29 -20t-81 -32t-127 -20t-124 18t-86 36l-27 18q-31 25 -60.5 27.5t-47.5 -9.5t-28 -32 q-22 -45 -2 -74.5t87 -73.5q83 -53 226 -67l-51 -52q-142 -142 -191 -190q-22 -22 -22 -52.5t22 -52.5l9 -9q22 -22 52.5 -22t52.5 22l191 191q114 -115 191 -191q22 -22 52.5 -22t52.5 22l9 9q22 22 22 52.5t-22 52.5l-191 190l-52 52q141 14 225 67q67 44 87 73.5t-2 74.5 zM1092 956q0 134 -95 229t-229 95t-229 -95t-95 -229t95 -229t229 -95t229 95t95 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1720" d="M1565 1408q65 0 110 -45.5t45 -110.5v-519q0 -176 -68 -336t-182.5 -275t-274 -182.5t-334.5 -67.5q-176 0 -335.5 67.5t-274.5 182.5t-183 275t-68 336v519q0 64 46 110t110 46h1409zM861 344q47 0 82 33l404 388q37 35 37 85q0 49 -34.5 83.5t-83.5 34.5q-47 0 -82 -33 l-323 -310l-323 310q-35 33 -81 33q-49 0 -83.5 -34.5t-34.5 -83.5q0 -51 36 -85l405 -388q33 -33 81 -33z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1494 -103l-295 695q-25 -49 -158.5 -305.5t-198.5 -389.5q-1 -1 -27.5 -0.5t-26.5 1.5q-82 193 -255.5 587t-259.5 596q-21 50 -66.5 107.5t-103.5 100.5t-102 43q0 5 -0.5 24t-0.5 27h583v-50q-39 -2 -79.5 -16t-66.5 -43t-10 -64q26 -59 216.5 -499t235.5 -540 q31 61 140 266.5t131 247.5q-19 39 -126 281t-136 295q-38 69 -201 71v50l513 -1v-47q-60 -2 -93.5 -25t-12.5 -69q33 -70 87 -189.5t86 -187.5q110 214 173 363q24 55 -10 79.5t-129 26.5q1 7 1 25v24q64 0 170.5 0.5t180 1t92.5 0.5v-49q-62 -2 -119 -33t-90 -81 l-213 -442q13 -33 127.5 -290t121.5 -274l441 1017q-14 38 -49.5 62.5t-65 31.5t-55.5 8v50l460 -4l1 -2l-1 -44q-139 -4 -201 -145q-526 -1216 -559 -1291h-49z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M949 643q0 -26 -16.5 -45t-41.5 -19q-26 0 -45 16.5t-19 41.5q0 26 17 45t42 19t44 -16.5t19 -41.5zM964 585l350 581q-9 -8 -67.5 -62.5t-125.5 -116.5t-136.5 -127t-117 -110.5t-50.5 -51.5l-349 -580q7 7 67 62t126 116.5t136 127t117 111t50 50.5zM1611 640 q0 -201 -104 -371q-3 2 -17 11t-26.5 16.5t-16.5 7.5q-13 0 -13 -13q0 -10 59 -44q-74 -112 -184.5 -190.5t-241.5 -110.5l-16 67q-1 10 -15 10q-5 0 -8 -5.5t-2 -9.5l16 -68q-72 -15 -146 -15q-199 0 -372 105q1 2 13 20.5t21.5 33.5t9.5 19q0 13 -13 13q-6 0 -17 -14.5 t-22.5 -34.5t-13.5 -23q-113 75 -192 187.5t-110 244.5l69 15q10 3 10 15q0 5 -5.5 8t-10.5 2l-68 -15q-14 72 -14 139q0 206 109 379q2 -1 18.5 -12t30 -19t17.5 -8q13 0 13 12q0 6 -12.5 15.5t-32.5 21.5l-20 12q77 112 189 189t244 107l15 -67q2 -10 15 -10q5 0 8 5.5 t2 10.5l-15 66q71 13 134 13q204 0 379 -109q-39 -56 -39 -65q0 -13 12 -13q11 0 48 64q111 -75 187.5 -186t107.5 -241l-56 -12q-10 -2 -10 -16q0 -5 5.5 -8t9.5 -2l57 13q14 -72 14 -140zM1696 640q0 163 -63.5 311t-170.5 255t-255 170.5t-311 63.5t-311 -63.5 t-255 -170.5t-170.5 -255t-63.5 -311t63.5 -311t170.5 -255t255 -170.5t311 -63.5t311 63.5t255 170.5t170.5 255t63.5 311zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191 t191 -286t71 -348z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M893 1536q240 2 451 -120q232 -134 352 -372l-742 39q-160 9 -294 -74.5t-185 -229.5l-276 424q128 159 311 245.5t383 87.5zM146 1131l337 -663q72 -143 211 -217t293 -45l-230 -451q-212 33 -385 157.5t-272.5 316t-99.5 411.5q0 267 146 491zM1732 962 q58 -150 59.5 -310.5t-48.5 -306t-153 -272t-246 -209.5q-230 -133 -498 -119l405 623q88 131 82.5 290.5t-106.5 277.5zM896 942q125 0 213.5 -88.5t88.5 -213.5t-88.5 -213.5t-213.5 -88.5t-213.5 88.5t-88.5 213.5t88.5 213.5t213.5 88.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M903 -256q-283 0 -504.5 150.5t-329.5 398.5q-58 131 -67 301t26 332.5t111 312t179 242.5l-11 -281q11 14 68 15.5t70 -15.5q42 81 160.5 138t234.5 59q-54 -45 -119.5 -148.5t-58.5 -163.5q25 -8 62.5 -13.5t63 -7.5t68 -4t50.5 -3q15 -5 9.5 -45.5t-30.5 -75.5 q-5 -7 -16.5 -18.5t-56.5 -35.5t-101 -34l15 -189l-139 67q-18 -43 -7.5 -81.5t36 -66.5t65.5 -41.5t81 -6.5q51 9 98 34.5t83.5 45t73.5 17.5q61 -4 89.5 -33t19.5 -65q-1 -2 -2.5 -5.5t-8.5 -12.5t-18 -15.5t-31.5 -10.5t-46.5 -1q-60 -95 -144.5 -135.5t-209.5 -29.5 q74 -61 162.5 -82.5t168.5 -6t154.5 52t128 87.5t80.5 104q43 91 39 192.5t-37.5 188.5t-78.5 125q87 -38 137 -79.5t77 -112.5q15 170 -57.5 343t-209.5 284q265 -77 412 -279.5t151 -517.5q2 -127 -40.5 -255t-123.5 -238t-189 -196t-247.5 -135.5t-288.5 -49.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1493 1308q-165 110 -359 110q-155 0 -293 -73t-240 -200q-75 -93 -119.5 -218t-48.5 -266v-42q4 -141 48.5 -266t119.5 -218q102 -127 240 -200t293 -73q194 0 359 110q-121 -108 -274.5 -168t-322.5 -60q-29 0 -43 1q-175 8 -333 82t-272 193t-181 281t-67 339 q0 182 71 348t191 286t286 191t348 71h3q168 -1 320.5 -60.5t273.5 -167.5zM1792 640q0 -192 -77 -362.5t-213 -296.5q-104 -63 -222 -63q-137 0 -255 84q154 56 253.5 233t99.5 405q0 227 -99 404t-253 234q119 83 254 83q119 0 226 -65q135 -125 210.5 -295t75.5 -361z " />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 599q0 -56 -7 -104h-1151q0 -146 109.5 -244.5t257.5 -98.5q99 0 185.5 46.5t136.5 130.5h423q-56 -159 -170.5 -281t-267.5 -188.5t-321 -66.5q-187 0 -356 83q-228 -116 -394 -116q-237 0 -237 263q0 115 45 275q17 60 109 229q199 360 475 606 q-184 -79 -427 -354q63 274 283.5 449.5t501.5 175.5q30 0 45 -1q255 117 433 117q64 0 116 -13t94.5 -40.5t66.5 -76.5t24 -115q0 -116 -75 -286q101 -182 101 -390zM1722 1239q0 83 -53 132t-137 49q-108 0 -254 -70q121 -47 222.5 -131.5t170.5 -195.5q51 135 51 216z M128 2q0 -86 48.5 -132.5t134.5 -46.5q115 0 266 83q-122 72 -213.5 183t-137.5 245q-98 -205 -98 -332zM632 715h728q-5 142 -113 237t-251 95q-144 0 -251.5 -95t-112.5 -237z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1792 288v960q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1248v-960q0 -66 -47 -113t-113 -47h-736v-128h352q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23 v64q0 14 9 23t23 9h352v128h-736q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M138 1408h197q-70 -64 -126 -149q-36 -56 -59 -115t-30 -125.5t-8.5 -120t10.5 -132t21 -126t28 -136.5q4 -19 6 -28q51 -238 81 -329q57 -171 152 -275h-272q-48 0 -82 34t-34 82v1304q0 48 34 82t82 34zM1346 1408h308q48 0 82 -34t34 -82v-1304q0 -48 -34 -82t-82 -34 h-178q212 210 196 565l-469 -101q-2 -45 -12 -82t-31 -72t-59.5 -59.5t-93.5 -36.5q-123 -26 -199 40q-32 27 -53 61t-51.5 129t-64.5 258q-35 163 -45.5 263t-5.5 139t23 77q20 41 62.5 73t102.5 45q45 12 83.5 6.5t67 -17t54 -35t43 -48t34.5 -56.5l468 100 q-68 175 -180 287z" />
|
||||
<glyph unicode="" d="M1401 -11l-6 -6q-113 -114 -259 -175q-154 -64 -317 -64q-165 0 -317 64q-148 63 -259 175q-113 112 -175 258q-42 103 -54 189q-4 28 48 36q51 8 56 -20q1 -1 1 -4q18 -90 46 -159q50 -124 152 -226q98 -98 226 -152q132 -56 276 -56q143 0 276 56q128 55 225 152l6 6 q10 10 25 6q12 -3 33 -22q36 -37 17 -58zM929 604l-66 -66l63 -63q21 -21 -7 -49q-17 -17 -32 -17q-10 0 -19 10l-62 61l-66 -66q-5 -5 -15 -5q-15 0 -31 16l-2 2q-18 15 -18 29q0 7 8 17l66 65l-66 66q-16 16 14 45q18 18 31 18q6 0 13 -5l65 -66l65 65q18 17 48 -13 q27 -27 11 -44zM1400 547q0 -118 -46 -228q-45 -105 -126 -186q-80 -80 -187 -126t-228 -46t-228 46t-187 126q-82 82 -125 186q-15 32 -15 40h-1q-9 27 43 44q50 16 60 -12q37 -99 97 -167h1v339v2q3 136 102 232q105 103 253 103q147 0 251 -103t104 -249 q0 -147 -104.5 -251t-250.5 -104q-58 0 -112 16q-28 11 -13 61q16 51 44 43l14 -3q14 -3 32.5 -6t30.5 -3q104 0 176 71.5t72 174.5q0 101 -72 171q-71 71 -175 71q-107 0 -178 -80q-64 -72 -64 -160v-413q110 -67 242 -67q96 0 185 36.5t156 103.5t103.5 155t36.5 183 q0 198 -141 339q-140 140 -339 140q-200 0 -340 -140q-53 -53 -77 -87l-2 -2q-8 -11 -13 -15.5t-21.5 -9.5t-38.5 3q-21 5 -36.5 16.5t-15.5 26.5v680q0 15 10.5 26.5t27.5 11.5h877q30 0 30 -55t-30 -55h-811v-483h1q40 42 102 84t108 61q109 46 231 46q121 0 228 -46 t187 -126q81 -81 126 -186q46 -112 46 -229zM1369 1128q9 -8 9 -18t-5.5 -18t-16.5 -21q-26 -26 -39 -26q-9 0 -16 7q-106 91 -207 133q-128 56 -276 56q-133 0 -262 -49q-27 -10 -45 37q-9 25 -8 38q3 16 16 20q130 57 299 57q164 0 316 -64q137 -58 235 -152z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1551 60q15 6 26 3t11 -17.5t-15 -33.5q-13 -16 -44 -43.5t-95.5 -68t-141 -74t-188 -58t-229.5 -24.5q-119 0 -238 31t-209 76.5t-172.5 104t-132.5 105t-84 87.5q-8 9 -10 16.5t1 12t8 7t11.5 2t11.5 -4.5q192 -117 300 -166q389 -176 799 -90q190 40 391 135z M1758 175q11 -16 2.5 -69.5t-28.5 -102.5q-34 -83 -85 -124q-17 -14 -26 -9t0 24q21 45 44.5 121.5t6.5 98.5q-5 7 -15.5 11.5t-27 6t-29.5 2.5t-35 0t-31.5 -2t-31 -3t-22.5 -2q-6 -1 -13 -1.5t-11 -1t-8.5 -1t-7 -0.5h-5.5h-4.5t-3 0.5t-2 1.5l-1.5 3q-6 16 47 40t103 30 q46 7 108 1t76 -24zM1364 618q0 -31 13.5 -64t32 -58t37.5 -46t33 -32l13 -11l-227 -224q-40 37 -79 75.5t-58 58.5l-19 20q-11 11 -25 33q-38 -59 -97.5 -102.5t-127.5 -63.5t-140 -23t-137.5 21t-117.5 65.5t-83 113t-31 162.5q0 84 28 154t72 116.5t106.5 83t122.5 57 t130 34.5t119.5 18.5t99.5 6.5v127q0 65 -21 97q-34 53 -121 53q-6 0 -16.5 -1t-40.5 -12t-56 -29.5t-56 -59.5t-48 -96l-294 27q0 60 22 119t67 113t108 95t151.5 65.5t190.5 24.5q100 0 181 -25t129.5 -61.5t81 -83t45 -86t12.5 -73.5v-589zM692 597q0 -86 70 -133 q66 -44 139 -22q84 25 114 123q14 45 14 101v162q-59 -2 -111 -12t-106.5 -33.5t-87 -71t-32.5 -114.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1536 1280q52 0 90 -38t38 -90v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128zM1152 1376v-288q0 -14 9 -23t23 -9 h64q14 0 23 9t9 23v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 1376v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM1536 -128v1024h-1408v-1024h1408zM896 448h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224 v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1152 416v-64q0 -14 -9 -23t-23 -9h-576q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h576q14 0 23 -9t9 -23zM128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23 t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47 t47 -113v-96h128q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1111 151l-46 -46q-9 -9 -22 -9t-23 9l-188 189l-188 -189q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22t9 23l189 188l-189 188q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l188 -188l188 188q10 9 23 9t22 -9l46 -46q9 -9 9 -22t-9 -23l-188 -188l188 -188q9 -10 9 -23t-9 -22z M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1303 572l-512 -512q-10 -9 -23 -9t-23 9l-288 288q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l220 -220l444 444q10 9 23 9t22 -9l46 -46q9 -9 9 -22t-9 -23zM128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23 t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47 t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M448 1536q26 0 45 -19t19 -45v-891l536 429q17 14 40 14q26 0 45 -19t19 -45v-379l536 429q17 14 40 14q26 0 45 -19t19 -45v-1152q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h384z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M512 448q66 0 128 15v-655q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v655q61 -15 128 -15zM512 1536q212 0 362 -150t150 -362t-150 -362t-362 -150t-362 150t-150 362t150 362t362 150zM512 1312q14 0 23 9t9 23t-9 23t-23 9q-146 0 -249 -103t-103 -249 q0 -14 9 -23t23 -9t23 9t9 23q0 119 84.5 203.5t203.5 84.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1745 1239q10 -10 10 -23t-10 -23l-141 -141q-28 -28 -68 -28h-1344q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h576v64q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-64h512q40 0 68 -28zM768 320h256v-512q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v512zM1600 768 q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-1344q-40 0 -68 28l-141 141q-10 10 -10 23t10 23l141 141q28 28 68 28h512v192h256v-192h576z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M2020 1525q28 -20 28 -53v-1408q0 -20 -11 -36t-29 -23l-640 -256q-24 -11 -48 0l-616 246l-616 -246q-10 -5 -24 -5q-19 0 -36 11q-28 20 -28 53v1408q0 20 11 36t29 23l640 256q24 11 48 0l616 -246l616 246q32 13 60 -6zM736 1390v-1270l576 -230v1270zM128 1173 v-1270l544 217v1270zM1920 107v1270l-544 -217v-1270z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M512 1536q13 0 22.5 -9.5t9.5 -22.5v-1472q0 -20 -17 -28l-480 -256q-7 -4 -15 -4q-13 0 -22.5 9.5t-9.5 22.5v1472q0 20 17 28l480 256q7 4 15 4zM1760 1536q13 0 22.5 -9.5t9.5 -22.5v-1472q0 -20 -17 -28l-480 -256q-7 -4 -15 -4q-13 0 -22.5 9.5t-9.5 22.5v1472 q0 20 17 28l480 256q7 4 15 4zM640 1536q8 0 14 -3l512 -256q18 -10 18 -29v-1472q0 -13 -9.5 -22.5t-22.5 -9.5q-8 0 -14 3l-512 256q-18 10 -18 29v1472q0 13 9.5 22.5t22.5 9.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M640 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 640q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-110 0 -211 18q-173 -173 -435 -229q-52 -10 -86 -13q-12 -1 -22 6t-13 18q-4 15 20 37q5 5 23.5 21.5t25.5 23.5t23.5 25.5t24 31.5t20.5 37 t20 48t14.5 57.5t12.5 72.5q-146 90 -229.5 216.5t-83.5 269.5q0 174 120 321.5t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M640 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5 t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51 t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 130 71 248.5t191 204.5t286 136.5t348 50.5t348 -50.5t286 -136.5t191 -204.5t71 -248.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M512 345l512 295v-591l-512 -296v592zM0 640v-591l512 296zM512 1527v-591l-512 -296v591zM512 936l512 295v-591z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1709 1018q-10 -236 -332 -651q-333 -431 -562 -431q-142 0 -240 263q-44 160 -132 482q-72 262 -157 262q-18 0 -127 -76l-77 98q24 21 108 96.5t130 115.5q156 138 241 146q95 9 153 -55.5t81 -203.5q44 -287 66 -373q55 -249 120 -249q51 0 154 161q101 161 109 246 q13 139 -109 139q-57 0 -121 -26q120 393 459 382q251 -8 236 -326z" />
|
||||
<glyph unicode="" d="M0 1408h1536v-1536h-1536v1536zM1085 293l-221 631l221 297h-634l221 -297l-221 -631l317 -304z" />
|
||||
<glyph unicode="" d="M0 1408h1536v-1536h-1536v1536zM908 1088l-12 -33l75 -83l-31 -114l25 -25l107 57l107 -57l25 25l-31 114l75 83l-12 33h-95l-53 96h-32l-53 -96h-95zM641 925q32 0 44.5 -16t11.5 -63l174 21q0 55 -17.5 92.5t-50.5 56t-69 25.5t-85 7q-133 0 -199 -57.5t-66 -182.5v-72 h-96v-128h76q20 0 20 -8v-382q0 -14 -5 -20t-18 -7l-73 -7v-88h448v86l-149 14q-6 1 -8.5 1.5t-3.5 2.5t-0.5 4t1 7t0.5 10v387h191l38 128h-231q-6 0 -2 6t4 9v80q0 27 1.5 40.5t7.5 28t19.5 20t36.5 5.5zM1248 96v86l-54 9q-7 1 -9.5 2.5t-2.5 3t1 7.5t1 12v520h-275 l-23 -101l83 -22q23 -7 23 -27v-370q0 -14 -6 -18.5t-20 -6.5l-70 -9v-86h352z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1792 690q0 -58 -29.5 -105.5t-79.5 -72.5q12 -46 12 -96q0 -155 -106.5 -287t-290.5 -208.5t-400 -76.5t-399.5 76.5t-290 208.5t-106.5 287q0 47 11 94q-51 25 -82 73.5t-31 106.5q0 82 58 140.5t141 58.5q85 0 145 -63q218 152 515 162l116 521q3 13 15 21t26 5 l369 -81q18 37 54 59.5t79 22.5q62 0 106 -43.5t44 -105.5t-44 -106t-106 -44t-105.5 43.5t-43.5 105.5l-334 74l-104 -472q300 -9 519 -160q58 61 143 61q83 0 141 -58.5t58 -140.5zM418 491q0 -62 43.5 -106t105.5 -44t106 44t44 106t-44 105.5t-106 43.5q-61 0 -105 -44 t-44 -105zM1228 136q11 11 11 26t-11 26q-10 10 -25 10t-26 -10q-41 -42 -121 -62t-160 -20t-160 20t-121 62q-11 10 -26 10t-25 -10q-11 -10 -11 -25.5t11 -26.5q43 -43 118.5 -68t122.5 -29.5t91 -4.5t91 4.5t122.5 29.5t118.5 68zM1225 341q62 0 105.5 44t43.5 106 q0 61 -44 105t-105 44q-62 0 -106 -43.5t-44 -105.5t44 -106t106 -44z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M69 741h1q16 126 58.5 241.5t115 217t167.5 176t223.5 117.5t276.5 43q231 0 414 -105.5t294 -303.5q104 -187 104 -442v-188h-1125q1 -111 53.5 -192.5t136.5 -122.5t189.5 -57t213 -3t208 46.5t173.5 84.5v-377q-92 -55 -229.5 -92t-312.5 -38t-316 53 q-189 73 -311.5 249t-124.5 372q-3 242 111 412t325 268q-48 -60 -78 -125.5t-46 -159.5h635q8 77 -8 140t-47 101.5t-70.5 66.5t-80.5 41t-75 20.5t-56 8.5l-22 1q-135 -5 -259.5 -44.5t-223.5 -104.5t-176 -140.5t-138 -163.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M0 32v608h2304v-608q0 -66 -47 -113t-113 -47h-1984q-66 0 -113 47t-47 113zM640 256v-128h384v128h-384zM256 256v-128h256v128h-256zM2144 1408q66 0 113 -47t47 -113v-224h-2304v224q0 66 47 113t113 47h1984z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1549 857q55 0 85.5 -28.5t30.5 -83.5t-34 -82t-91 -27h-136v-177h-25v398h170zM1710 267l-4 -11l-5 -10q-113 -230 -330.5 -366t-474.5 -136q-182 0 -348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71q244 0 454.5 -124t329.5 -338l2 -4l8 -16 q-30 -15 -136.5 -68.5t-163.5 -84.5q-6 -3 -479 -268q384 -183 799 -366zM896 -234q250 0 462.5 132.5t322.5 357.5l-287 129q-72 -140 -206 -222t-292 -82q-151 0 -280 75t-204 204t-75 280t75 280t204 204t280 75t280 -73.5t204 -204.5l280 143q-116 208 -321 329 t-443 121q-119 0 -232.5 -31.5t-209 -87.5t-176.5 -137t-137 -176.5t-87.5 -209t-31.5 -232.5t31.5 -232.5t87.5 -209t137 -176.5t176.5 -137t209 -87.5t232.5 -31.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1427 827l-614 386l92 151h855zM405 562l-184 116v858l1183 -743zM1424 697l147 -95v-858l-532 335zM1387 718l-500 -802h-855l356 571z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M640 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1152 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1664 496v-752h-640v320q0 80 -56 136t-136 56t-136 -56t-56 -136v-320h-640v752q0 16 16 16h96 q16 0 16 -16v-112h128v624q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h16v393q-32 19 -32 55q0 26 19 45t45 19t45 -19t19 -45q0 -36 -32 -55v-9h272q16 0 16 -16v-224q0 -16 -16 -16h-272v-128h16q16 0 16 -16v-112h128 v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-624h128v112q0 16 16 16h96q16 0 16 -16z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M2288 731q16 -8 16 -27t-16 -27l-320 -192q-8 -5 -16 -5q-9 0 -16 4q-16 10 -16 28v128h-858q37 -58 83 -165q16 -37 24.5 -55t24 -49t27 -47t27 -34t31.5 -26t33 -8h96v96q0 14 9 23t23 9h320q14 0 23 -9t9 -23v-320q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v96h-96 q-32 0 -61 10t-51 23.5t-45 40.5t-37 46t-33.5 57t-28.5 57.5t-28 60.5q-23 53 -37 81.5t-36 65t-44.5 53.5t-46.5 17h-360q-22 -84 -91 -138t-157 -54q-106 0 -181 75t-75 181t75 181t181 75q88 0 157 -54t91 -138h104q24 0 46.5 17t44.5 53.5t36 65t37 81.5q19 41 28 60.5 t28.5 57.5t33.5 57t37 46t45 40.5t51 23.5t61 10h107q21 57 70 92.5t111 35.5q80 0 136 -56t56 -136t-56 -136t-136 -56q-62 0 -111 35.5t-70 92.5h-107q-17 0 -33 -8t-31.5 -26t-27 -34t-27 -47t-24 -49t-24.5 -55q-46 -107 -83 -165h1114v128q0 18 16 28t32 -1z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1150 774q0 -56 -39.5 -95t-95.5 -39h-253v269h253q56 0 95.5 -39.5t39.5 -95.5zM1329 774q0 130 -91.5 222t-222.5 92h-433v-896h180v269h253q130 0 222 91.5t92 221.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348 t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1645 438q0 59 -34 106.5t-87 68.5q-7 -45 -23 -92q-7 -24 -27.5 -38t-44.5 -14q-12 0 -24 3q-31 10 -45 38.5t-4 58.5q23 71 23 143q0 123 -61 227.5t-166 165.5t-228 61q-134 0 -247 -73t-167 -194q108 -28 188 -106q22 -23 22 -55t-22 -54t-54 -22t-55 22 q-75 75 -180 75q-106 0 -181 -74.5t-75 -180.5t75 -180.5t181 -74.5h1046q79 0 134.5 55.5t55.5 133.5zM1798 438q0 -142 -100.5 -242t-242.5 -100h-1046q-169 0 -289 119.5t-120 288.5q0 153 100 267t249 136q62 184 221 298t354 114q235 0 408.5 -158.5t196.5 -389.5 q116 -25 192.5 -118.5t76.5 -214.5zM2048 438q0 -175 -97 -319q-23 -33 -64 -33q-24 0 -43 13q-26 17 -32 48.5t12 57.5q71 104 71 233t-71 233q-18 26 -12 57t32 49t57.5 11.5t49.5 -32.5q97 -142 97 -318zM2304 438q0 -244 -134 -443q-23 -34 -64 -34q-23 0 -42 13 q-26 18 -32.5 49t11.5 57q108 164 108 358q0 195 -108 357q-18 26 -11.5 57.5t32.5 48.5q26 18 57 12t49 -33q134 -198 134 -442z" />
|
||||
<glyph unicode="" d="M1500 -13q0 -89 -63 -152.5t-153 -63.5t-153.5 63.5t-63.5 152.5q0 90 63.5 153.5t153.5 63.5t153 -63.5t63 -153.5zM1267 268q-115 -15 -192.5 -102.5t-77.5 -205.5q0 -74 33 -138q-146 -78 -379 -78q-109 0 -201 21t-153.5 54.5t-110.5 76.5t-76 85t-44.5 83 t-23.5 66.5t-6 39.5q0 19 4.5 42.5t18.5 56t36.5 58t64 43.5t94.5 18t94 -17.5t63 -41t35.5 -53t17.5 -49t4 -33.5q0 -34 -23 -81q28 -27 82 -42t93 -17l40 -1q115 0 190 51t75 133q0 26 -9 48.5t-31.5 44.5t-49.5 41t-74 44t-93.5 47.5t-119.5 56.5q-28 13 -43 20 q-116 55 -187 100t-122.5 102t-72 125.5t-20.5 162.5q0 78 20.5 150t66 137.5t112.5 114t166.5 77t221.5 28.5q120 0 220 -26t164.5 -67t109.5 -94t64 -105.5t19 -103.5q0 -46 -15 -82.5t-36.5 -58t-48.5 -36t-49 -19.5t-39 -5h-8h-32t-39 5t-44 14t-41 28t-37 46t-24 70.5 t-10 97.5q-15 16 -59 25.5t-81 10.5l-37 1q-68 0 -117.5 -31t-70.5 -70t-21 -76q0 -24 5 -43t24 -46t53 -51t97 -53.5t150 -58.5q76 -25 138.5 -53.5t109 -55.5t83 -59t60.5 -59.5t41 -62.5t26.5 -62t14.5 -63.5t6 -62t1 -62.5z" />
|
||||
<glyph unicode="" d="M704 352v576q0 14 -9 23t-23 9h-256q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h256q14 0 23 9t9 23zM1152 352v576q0 14 -9 23t-23 9h-256q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h256q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103 t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM864 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-192zM480 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-192z" />
|
||||
<glyph unicode="" d="M1088 352v576q0 14 -9 23t-23 9h-576q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h576q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
|
||||
<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM480 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h576q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-576z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1757 128l35 -313q3 -28 -16 -50q-19 -21 -48 -21h-1664q-29 0 -48 21q-19 22 -16 50l35 313h1722zM1664 967l86 -775h-1708l86 775q3 24 21 40.5t43 16.5h256v-128q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5v128h384v-128q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5v128h256q25 0 43 -16.5t21 -40.5zM1280 1152v-256q0 -26 -19 -45t-45 -19t-45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-256q0 -26 -19 -45t-45 -19t-45 19t-19 45v256q0 159 112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" />
|
||||
<glyph unicode="" horiz-adv-x="2048" d="M1920 768q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5h-15l-115 -662q-8 -46 -44 -76t-82 -30h-1280q-46 0 -82 30t-44 76l-115 662h-15q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5h1792zM485 -32q26 2 43.5 22.5t15.5 46.5l-32 416q-2 26 -22.5 43.5 t-46.5 15.5t-43.5 -22.5t-15.5 -46.5l32 -416q2 -25 20.5 -42t43.5 -17h5zM896 32v416q0 26 -19 45t-45 19t-45 -19t-19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45zM1280 32v416q0 26 -19 45t-45 19t-45 -19t-19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45zM1632 27l32 416 q2 26 -15.5 46.5t-43.5 22.5t-46.5 -15.5t-22.5 -43.5l-32 -416q-2 -26 15.5 -46.5t43.5 -22.5h5q25 0 43.5 17t20.5 42zM476 1244l-93 -412h-132l101 441q19 88 89 143.5t160 55.5h167q0 26 19 45t45 19h384q26 0 45 -19t19 -45h167q90 0 160 -55.5t89 -143.5l101 -441 h-132l-93 412q-11 44 -45.5 72t-79.5 28h-167q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45h-167q-45 0 -79.5 -28t-45.5 -72z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M991 512l64 256h-254l-64 -256h254zM1759 1016l-56 -224q-7 -24 -31 -24h-327l-64 -256h311q15 0 25 -12q10 -14 6 -28l-56 -224q-5 -24 -31 -24h-327l-81 -328q-7 -24 -31 -24h-224q-16 0 -26 12q-9 12 -6 28l78 312h-254l-81 -328q-7 -24 -31 -24h-225q-15 0 -25 12 q-9 12 -6 28l78 312h-311q-15 0 -25 12q-9 12 -6 28l56 224q7 24 31 24h327l64 256h-311q-15 0 -25 12q-10 14 -6 28l56 224q5 24 31 24h327l81 328q7 24 32 24h224q15 0 25 -12q9 -12 6 -28l-78 -312h254l81 328q7 24 32 24h224q15 0 25 -12q9 -12 6 -28l-78 -312h311 q15 0 25 -12q9 -12 6 -28z" />
|
||||
<glyph unicode="" d="M841 483l148 -148l-149 -149zM840 1094l149 -149l-148 -148zM710 -130l464 464l-306 306l306 306l-464 464v-611l-255 255l-93 -93l320 -321l-320 -321l93 -93l255 255v-611zM1429 640q0 -209 -32 -365.5t-87.5 -257t-140.5 -162.5t-181.5 -86.5t-219.5 -24.5 t-219.5 24.5t-181.5 86.5t-140.5 162.5t-87.5 257t-32 365.5t32 365.5t87.5 257t140.5 162.5t181.5 86.5t219.5 24.5t219.5 -24.5t181.5 -86.5t140.5 -162.5t87.5 -257t32 -365.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1024" d="M596 113l173 172l-173 172v-344zM596 823l173 172l-173 172v-344zM628 640l356 -356l-539 -540v711l-297 -296l-108 108l372 373l-372 373l108 108l297 -296v711l539 -540z" />
|
||||
<glyph unicode="" d="M1280 256q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM512 1024q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5 t112.5 -271.5zM1440 1344q0 -20 -13 -38l-1056 -1408q-19 -26 -51 -26h-160q-26 0 -45 19t-19 45q0 20 13 38l1056 1408q19 26 51 26h160q26 0 45 -19t19 -45zM768 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M104 830l792 -1015l-868 630q-18 13 -25 34.5t0 42.5l101 308v0zM566 830h660l-330 -1015v0zM368 1442l198 -612h-462l198 612q8 23 33 23t33 -23zM1688 830l101 -308q7 -21 0 -42.5t-25 -34.5l-868 -630l792 1015v0zM1688 830h-462l198 612q8 23 33 23t33 -23z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M384 704h160v224h-160v-224zM1221 372v92q-104 -36 -243 -38q-135 -1 -259.5 46.5t-220.5 122.5l1 -96q88 -80 212 -128.5t272 -47.5q129 0 238 49zM640 704h640v224h-640v-224zM1792 736q0 -187 -99 -352q89 -102 89 -229q0 -157 -129.5 -268t-313.5 -111 q-122 0 -225 52.5t-161 140.5q-19 -1 -57 -1t-57 1q-58 -88 -161 -140.5t-225 -52.5q-184 0 -313.5 111t-129.5 268q0 127 89 229q-99 165 -99 352q0 209 120 385.5t326.5 279.5t449.5 103t449.5 -103t326.5 -279.5t120 -385.5z" />
|
||||
<glyph unicode="" d="M515 625v-128h-252v128h252zM515 880v-127h-252v127h252zM1273 369v-128h-341v128h341zM1273 625v-128h-672v128h672zM1273 880v-127h-672v127h672zM1408 20v1240q0 8 -6 14t-14 6h-32l-378 -256l-210 171l-210 -171l-378 256h-32q-8 0 -14 -6t-6 -14v-1240q0 -8 6 -14 t14 -6h1240q8 0 14 6t6 14zM553 1130l185 150h-406zM983 1130l221 150h-406zM1536 1260v-1240q0 -62 -43 -105t-105 -43h-1240q-62 0 -105 43t-43 105v1240q0 62 43 105t105 43h1240q62 0 105 -43t43 -105z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M896 720q-104 196 -160 278q-139 202 -347 318q-34 19 -70 36q-89 40 -94 32t34 -38l39 -31q62 -43 112.5 -93.5t94.5 -116.5t70.5 -113t70.5 -131q9 -17 13 -25q44 -84 84 -153t98 -154t115.5 -150t131 -123.5t148.5 -90.5q153 -66 154 -60q1 3 -49 37q-53 36 -81 57 q-77 58 -179 211t-185 310zM549 177q-76 60 -132.5 125t-98 143.5t-71 154.5t-58.5 186t-52 209t-60.5 252t-76.5 289q273 0 497.5 -36t379 -92t271 -144.5t185.5 -172.5t110 -198.5t56 -199.5t12.5 -198.5t-9.5 -173t-20 -143.5t-13 -107l323 -327h-104l-281 285 q-22 -2 -91.5 -14t-121.5 -19t-138 -6t-160.5 17t-167.5 59t-179 111z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1374 879q-6 26 -28.5 39.5t-48.5 7.5q-261 -62 -401 -62t-401 62q-26 6 -48.5 -7.5t-28.5 -39.5t7.5 -48.5t39.5 -28.5q194 -46 303 -58q-2 -158 -15.5 -269t-26.5 -155.5t-41 -115.5l-9 -21q-10 -25 1 -49t36 -34q9 -4 23 -4q44 0 60 41l8 20q54 139 71 259h42 q17 -120 71 -259l8 -20q16 -41 60 -41q14 0 23 4q25 10 36 34t1 49l-9 21q-28 71 -41 115.5t-26.5 155.5t-15.5 269q109 12 303 58q26 6 39.5 28.5t7.5 48.5zM1024 1024q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1600 640q0 -143 -55.5 -273.5t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5zM896 1408q-156 0 -298 -61t-245 -164t-164 -245t-61 -298t61 -298 t164 -245t245 -164t298 -61t298 61t245 164t164 245t61 298t-61 298t-164 245t-245 164t-298 61zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
|
||||
<glyph unicode="" d="M1438 723q34 -35 29 -82l-44 -551q-4 -42 -34.5 -70t-71.5 -28q-6 0 -9 1q-44 3 -72.5 36.5t-25.5 77.5l35 429l-143 -8q55 -113 55 -240q0 -216 -148 -372l-137 137q91 101 91 235q0 145 -102.5 248t-247.5 103q-134 0 -236 -92l-137 138q120 114 284 141l264 300 l-149 87l-181 -161q-33 -30 -77 -27.5t-73 35.5t-26.5 77t34.5 73l239 213q26 23 60 26.5t64 -14.5l488 -283q36 -21 48 -68q17 -67 -26 -117l-205 -232l371 20q49 3 83 -32zM1240 1180q-74 0 -126 52t-52 126t52 126t126 52t126.5 -52t52.5 -126t-52.5 -126t-126.5 -52z M613 -62q106 0 196 61l139 -139q-146 -116 -335 -116q-148 0 -273.5 73t-198.5 198t-73 273q0 188 116 336l139 -139q-60 -88 -60 -197q0 -145 102.5 -247.5t247.5 -102.5z" />
|
||||
<glyph unicode="" d="M880 336v-160q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v160q0 14 9 23t23 9h160q14 0 23 -9t9 -23zM1136 832q0 -50 -15 -90t-45.5 -69t-52 -44t-59.5 -36q-32 -18 -46.5 -28t-26 -24t-11.5 -29v-32q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v68q0 35 10.5 64.5 t24 47.5t39 35.5t41 25.5t44.5 21q53 25 75 43t22 49q0 42 -43.5 71.5t-95.5 29.5q-56 0 -95 -27q-29 -20 -80 -83q-9 -12 -25 -12q-11 0 -19 6l-108 82q-10 7 -12 20t5 23q122 192 349 192q129 0 238.5 -89.5t109.5 -214.5zM768 1280q-130 0 -248.5 -51t-204 -136.5 t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5 t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M366 1225q-64 0 -110 45.5t-46 110.5q0 64 46 109.5t110 45.5t109.5 -45.5t45.5 -109.5q0 -65 -45.5 -110.5t-109.5 -45.5zM917 583q0 -50 -30 -67.5t-63.5 -6.5t-47.5 34l-367 438q-7 12 -14 15.5t-11 1.5l-3 -3q-7 -8 4 -21l122 -139l1 -354l-161 -457 q-67 -192 -92 -234q-16 -26 -28 -32q-50 -26 -103 -1q-29 13 -41.5 43t-9.5 57q2 17 197 618l5 416l-85 -164l35 -222q4 -24 -1 -42t-14 -27.5t-19 -16t-17 -7.5l-7 -2q-19 -3 -34.5 3t-24 16t-14 22t-7.5 19.5t-2 9.5l-46 299l211 381q23 34 113 34q75 0 107 -40l424 -521 q7 -5 14 -17l3 -3l-1 -1q7 -13 7 -29zM514 433q43 -113 88.5 -225t69.5 -168l24 -55q36 -93 42 -125q11 -70 -36 -97q-35 -22 -66 -16t-51 22t-29 35h-1q-6 16 -8 25l-124 351zM1338 -159q31 -49 31 -57q0 -5 -3 -7q-9 -5 -14.5 0.5t-15.5 26t-16 30.5q-114 172 -423 661 q3 -1 7 1t7 4l3 2q11 9 11 17z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M504 542h171l-1 265zM1530 641q0 87 -50.5 140t-146.5 53h-54v-388h52q91 0 145 57t54 138zM956 1018l1 -756q0 -14 -9.5 -24t-23.5 -10h-216q-14 0 -23.5 10t-9.5 24v62h-291l-55 -81q-10 -15 -28 -15h-267q-21 0 -30.5 18t3.5 35l556 757q9 14 27 14h332q14 0 24 -10 t10 -24zM1783 641q0 -193 -125.5 -303t-324.5 -110h-270q-14 0 -24 10t-10 24v756q0 14 10 24t24 10h268q200 0 326 -109t126 -302zM1939 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-44.5 -108t-73.5 -102.5h-51q38 45 66.5 104.5t41.5 112t21 98t9 72.5l1 27q0 8 -0.5 22.5 t-7.5 60t-20 91.5t-41 111.5t-66 124.5h43q41 -47 72 -107t45.5 -111.5t23 -96t10.5 -70.5zM2123 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-45 -108t-74 -102.5h-51q38 45 66.5 104.5t41.5 112t21 98t9 72.5l1 27q0 8 -0.5 22.5t-7.5 60t-19.5 91.5t-40.5 111.5t-66 124.5 h43q41 -47 72 -107t45.5 -111.5t23 -96t10.5 -70.5zM2304 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-44.5 -108t-73.5 -102.5h-51q38 45 66 104.5t41 112t21 98t9 72.5l1 27q0 8 -0.5 22.5t-7.5 60t-19.5 91.5t-40.5 111.5t-66 124.5h43q41 -47 72 -107t45.5 -111.5t23 -96 t9.5 -70.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1408" d="M617 -153q0 11 -13 58t-31 107t-20 69q-1 4 -5 26.5t-8.5 36t-13.5 21.5q-15 14 -51 14q-23 0 -70 -5.5t-71 -5.5q-34 0 -47 11q-6 5 -11 15.5t-7.5 20t-6.5 24t-5 18.5q-37 128 -37 255t37 255q1 4 5 18.5t6.5 24t7.5 20t11 15.5q13 11 47 11q24 0 71 -5.5t70 -5.5 q36 0 51 14q9 8 13.5 21.5t8.5 36t5 26.5q2 9 20 69t31 107t13 58q0 22 -43.5 52.5t-75.5 42.5q-20 8 -45 8q-34 0 -98 -18q-57 -17 -96.5 -40.5t-71 -66t-46 -70t-45.5 -94.5q-6 -12 -9 -19q-49 -107 -68 -216t-19 -244t19 -244t68 -216q56 -122 83 -161q63 -91 179 -127 l6 -2q64 -18 98 -18q25 0 45 8q32 12 75.5 42.5t43.5 52.5zM776 760q-26 0 -45 19t-19 45.5t19 45.5q37 37 37 90q0 52 -37 91q-19 19 -19 45t19 45t45 19t45 -19q75 -75 75 -181t-75 -181q-21 -19 -45 -19zM957 579q-27 0 -45 19q-19 19 -19 45t19 45q112 114 112 272 t-112 272q-19 19 -19 45t19 45t45 19t45 -19q150 -150 150 -362t-150 -362q-18 -19 -45 -19zM1138 398q-27 0 -45 19q-19 19 -19 45t19 45q90 91 138.5 208t48.5 245t-48.5 245t-138.5 208q-19 19 -19 45t19 45t45 19t45 -19q109 -109 167 -249t58 -294t-58 -294t-167 -249 q-18 -19 -45 -19z" />
|
||||
<glyph unicode="" horiz-adv-x="2176" d="M192 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM704 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM704 864q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1472 352 q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1472 864q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 864 q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 1376q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 192q0 -80 -56 -136 t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 1216q0 -80 -56 -136t-136 -56 t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM2176 192q0 -80 -56 -136t-136 -56t-136 56 t-56 136t56 136t136 56t136 -56t56 -136zM1664 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM2176 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136 t56 136t136 56t136 -56t56 -136zM2176 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M128 -192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM320 0q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM365 365l256 -256l-90 -90l-256 256zM704 384q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45z M1411 704q0 -59 -11.5 -108.5t-37.5 -93.5t-44 -67.5t-53 -64.5q-31 -35 -45.5 -54t-33.5 -50t-26.5 -64t-7.5 -74q0 -159 -112.5 -271.5t-271.5 -112.5q-26 0 -45 19t-19 45t19 45t45 19q106 0 181 75t75 181q0 57 11.5 105.5t37 91t43.5 66.5t52 63q40 46 59.5 72 t37.5 74.5t18 103.5q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM896 576q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45 t45 19t45 -19t19 -45zM1184 704q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 93 -65.5 158.5t-158.5 65.5q-92 0 -158 -65.5t-66 -158.5q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 146 103 249t249 103t249 -103t103 -249zM1578 993q10 -25 -1 -49t-36 -34q-9 -4 -23 -4 q-19 0 -35.5 11t-23.5 30q-68 178 -224 295q-21 16 -25 42t12 47q17 21 43 25t47 -12q183 -137 266 -351zM1788 1074q9 -25 -1.5 -49t-35.5 -34q-11 -4 -23 -4q-44 0 -60 41q-92 238 -297 393q-22 16 -25.5 42t12.5 47q16 22 42 25.5t47 -12.5q235 -175 341 -449z" />
|
||||
<glyph unicode="" horiz-adv-x="2304" d="M1032 576q-59 2 -84 55q-17 34 -48 53.5t-68 19.5q-53 0 -90.5 -37.5t-37.5 -90.5q0 -56 36 -89l10 -8q34 -31 82 -31q37 0 68 19.5t48 53.5q25 53 84 55zM1600 704q0 56 -36 89l-10 8q-34 31 -82 31q-37 0 -68 -19.5t-48 -53.5q-25 -53 -84 -55q59 -2 84 -55 q17 -34 48 -53.5t68 -19.5q53 0 90.5 37.5t37.5 90.5zM1174 925q-17 -35 -55 -48t-73 4q-62 31 -134 31q-51 0 -99 -17q3 0 9.5 0.5t9.5 0.5q92 0 170.5 -50t118.5 -133q17 -36 3.5 -73.5t-49.5 -54.5q-18 -9 -39 -9q21 0 39 -9q36 -17 49.5 -54.5t-3.5 -73.5 q-40 -83 -118.5 -133t-170.5 -50h-6q-16 2 -44 4l-290 27l-239 -120q-14 -7 -29 -7q-40 0 -57 35l-160 320q-11 23 -4 47.5t29 37.5l209 119l148 267q17 155 91.5 291.5t195.5 236.5q31 25 70.5 21.5t64.5 -34.5t21.5 -70t-34.5 -65q-70 -59 -117 -128q123 84 267 101 q40 5 71.5 -19t35.5 -64q5 -40 -19 -71.5t-64 -35.5q-84 -10 -159 -55q46 10 99 10q115 0 218 -50q36 -18 49 -55.5t-5 -73.5zM2137 1085l160 -320q11 -23 4 -47.5t-29 -37.5l-209 -119l-148 -267q-17 -155 -91.5 -291.5t-195.5 -236.5q-26 -22 -61 -22q-45 0 -74 35 q-25 31 -21.5 70t34.5 65q70 59 117 128q-123 -84 -267 -101q-4 -1 -12 -1q-36 0 -63.5 24t-31.5 60q-5 40 19 71.5t64 35.5q84 10 159 55q-46 -10 -99 -10q-115 0 -218 50q-36 18 -49 55.5t5 73.5q17 35 55 48t73 -4q62 -31 134 -31q51 0 99 17q-3 0 -9.5 -0.5t-9.5 -0.5 q-92 0 -170.5 50t-118.5 133q-17 36 -3.5 73.5t49.5 54.5q18 9 39 9q-21 0 -39 9q-36 17 -49.5 54.5t3.5 73.5q40 83 118.5 133t170.5 50h6h1q14 -2 42 -4l291 -27l239 120q14 7 29 7q40 0 57 -35z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M1056 704q0 -26 19 -45t45 -19t45 19t19 45q0 146 -103 249t-249 103t-249 -103t-103 -249q0 -26 19 -45t45 -19t45 19t19 45q0 93 66 158.5t158 65.5t158 -65.5t66 -158.5zM835 1280q-117 0 -223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5q0 -26 19 -45t45 -19t45 19 t19 45q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -55 -18 -103.5t-37.5 -74.5t-59.5 -72q-34 -39 -52 -63t-43.5 -66.5t-37 -91t-11.5 -105.5q0 -106 -75 -181t-181 -75q-26 0 -45 -19t-19 -45t19 -45t45 -19q159 0 271.5 112.5t112.5 271.5q0 41 7.5 74 t26.5 64t33.5 50t45.5 54q35 41 53 64.5t44 67.5t37.5 93.5t11.5 108.5q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5zM591 561l226 -226l-579 -579q-12 -12 -29 -12t-29 12l-168 168q-12 12 -12 29t12 29zM1612 1524l168 -168q12 -12 12 -29t-12 -30l-233 -233 l-26 -25l-71 -71q-66 153 -195 258l91 91l207 207q13 12 30 12t29 -12z" />
|
||||
<glyph unicode="" d="M866 1021q0 -27 -13 -94q-11 -50 -31.5 -150t-30.5 -150q-2 -11 -4.5 -12.5t-13.5 -2.5q-20 -2 -31 -2q-58 0 -84 49.5t-26 113.5q0 88 35 174t103 124q28 14 51 14q28 0 36.5 -16.5t8.5 -47.5zM1352 597q0 14 -39 75.5t-52 66.5q-21 8 -34 8q-91 0 -226 -77l-2 2 q3 22 27.5 135t24.5 178q0 233 -242 233q-24 0 -68 -6q-94 -17 -168.5 -89.5t-111.5 -166.5t-37 -189q0 -146 80.5 -225t227.5 -79q25 0 25 -3t-1 -5q-4 -34 -26 -117q-14 -52 -51.5 -101t-82.5 -49q-42 0 -42 47q0 24 10.5 47.5t25 39.5t29.5 28.5t26 20t11 8.5q0 3 -7 10 q-24 22 -58.5 36.5t-65.5 14.5q-35 0 -63.5 -34t-41 -75t-12.5 -75q0 -88 51.5 -142t138.5 -54q82 0 155 53t117.5 126t65.5 153q6 22 15.5 66.5t14.5 66.5q3 12 14 18q118 60 227 60q48 0 127 -18q1 -1 4 -1q5 0 9.5 4.5t4.5 8.5zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1535" d="M744 1231q0 24 -2 38.5t-8.5 30t-21 23t-37.5 7.5q-39 0 -78 -23q-105 -58 -159 -190.5t-54 -269.5q0 -44 8.5 -85.5t26.5 -80.5t52.5 -62.5t81.5 -23.5q4 0 18 -0.5t20 0t16 3t15 8.5t7 16q16 77 48 231.5t48 231.5q19 91 19 146zM1498 575q0 -7 -7.5 -13.5t-15.5 -6.5 l-6 1q-22 3 -62 11t-72 12.5t-63 4.5q-167 0 -351 -93q-15 -8 -21 -27q-10 -36 -24.5 -105.5t-22.5 -100.5q-23 -91 -70 -179.5t-112.5 -164.5t-154.5 -123t-185 -47q-135 0 -214.5 83.5t-79.5 219.5q0 53 19.5 117t63 116.5t97.5 52.5q38 0 120 -33.5t83 -61.5 q0 -1 -16.5 -12.5t-39.5 -31t-46 -44.5t-39 -61t-16 -74q0 -33 16.5 -53t48.5 -20q45 0 85 31.5t66.5 78t48 105.5t32.5 107t16 90v9q0 2 -3.5 3.5t-8.5 1.5h-10t-10 -0.5t-6 -0.5q-227 0 -352 122.5t-125 348.5q0 108 34.5 221t96 210t156 167.5t204.5 89.5q52 9 106 9 q374 0 374 -360q0 -98 -38 -273t-43 -211l3 -3q101 57 182.5 88t167.5 31q22 0 53 -13q19 -7 80 -102.5t61 -116.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M831 863q32 0 59 -18l222 -148q61 -40 110 -97l146 -170q40 -46 29 -106l-72 -413q-6 -32 -29.5 -53.5t-55.5 -25.5l-527 -56l-352 -32h-9q-39 0 -67.5 28t-28.5 68q0 37 27 64t65 32l260 32h-448q-41 0 -69.5 30t-26.5 71q2 39 32 65t69 26l442 1l-521 64q-41 5 -66 37 t-19 73q6 35 34.5 57.5t65.5 22.5h10l481 -60l-351 94q-38 10 -62 41.5t-18 68.5q6 36 33 58.5t62 22.5q6 0 20 -2l448 -96l217 -37q1 0 3 -0.5t3 -0.5q23 0 30.5 23t-12.5 36l-186 125q-35 23 -42 63.5t18 73.5q27 38 76 38zM761 661l186 -125l-218 37l-5 2l-36 38 l-238 262q-1 1 -2.5 3.5t-2.5 3.5q-24 31 -18.5 70t37.5 64q31 23 68 17.5t64 -33.5l142 -147l-4 -4t-5 -4q-32 -45 -23 -99t55 -85zM1648 1115l15 -266q4 -73 -11 -147l-48 -219q-12 -59 -67 -87l-106 -54q2 62 -39 109l-146 170q-53 61 -117 103l-222 148q-34 23 -76 23 q-51 0 -88 -37l-235 312q-25 33 -18 73.5t41 63.5q33 22 71.5 14t62.5 -40l266 -352l-262 455q-21 35 -10.5 75t47.5 59q35 18 72.5 6t57.5 -46l241 -420l-136 337q-15 35 -4.5 74t44.5 56q37 19 76 6t56 -51l193 -415l101 -196q8 -15 23 -17.5t27 7.5t11 26l-12 224 q-2 41 26 71t69 31q39 0 67 -28.5t30 -67.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" d="M335 180q-2 0 -6 2q-86 57 -168.5 145t-139.5 180q-21 30 -21 69q0 9 2 19t4 18t7 18t8.5 16t10.5 17t10 15t12 15.5t11 14.5q184 251 452 365q-110 198 -110 211q0 19 17 29q116 64 128 64q18 0 28 -16l124 -229q92 19 192 19q266 0 497.5 -137.5t378.5 -369.5 q20 -31 20 -69t-20 -69q-91 -142 -218.5 -253.5t-278.5 -175.5q110 -198 110 -211q0 -20 -17 -29q-116 -64 -127 -64q-19 0 -29 16l-124 229l-64 119l-444 820l7 7q-58 -24 -99 -47q3 -5 127 -234t243 -449t119 -223q0 -7 -9 -9q-13 -3 -72 -3q-57 0 -60 7l-456 841 q-39 -28 -82 -68q24 -43 214 -393.5t190 -354.5q0 -10 -11 -10q-14 0 -82.5 22t-72.5 28l-106 197l-224 413q-44 -53 -78 -106q2 -3 18 -25t23 -34l176 -327q0 -10 -10 -10zM1165 282l49 -91q273 111 450 385q-180 277 -459 389q67 -64 103 -148.5t36 -176.5 q0 -106 -47 -200.5t-132 -157.5zM848 896q0 -20 14 -34t34 -14q86 0 147 -61t61 -147q0 -20 14 -34t34 -14t34 14t14 34q0 126 -89 215t-215 89q-20 0 -34 -14t-14 -34zM1214 961l-9 4l7 -7z" />
|
||||
<glyph unicode="" horiz-adv-x="1280" d="M1050 430q0 -215 -147 -374q-148 -161 -378 -161q-232 0 -378 161q-147 159 -147 374q0 147 68 270.5t189 196.5t268 73q96 0 182 -31q-32 -62 -39 -126q-66 28 -143 28q-167 0 -280.5 -123t-113.5 -291q0 -170 112.5 -288.5t281.5 -118.5t281 118.5t112 288.5 q0 89 -32 166q66 13 123 49q41 -98 41 -212zM846 619q0 -192 -79.5 -345t-238.5 -253l-14 -1q-29 0 -62 5q83 32 146.5 102.5t99.5 154.5t58.5 189t30 192.5t7.5 178.5q0 69 -3 103q55 -160 55 -326zM791 947v-2q-73 214 -206 440q88 -59 142.5 -186.5t63.5 -251.5z M1035 744q-83 0 -160 75q218 120 290 247q19 37 21 56q-42 -94 -139.5 -166.5t-204.5 -97.5q-35 54 -35 113q0 37 17 79t43 68q46 44 157 74q59 16 106 58.5t74 100.5q74 -105 74 -253q0 -109 -24 -170q-32 -77 -88.5 -130.5t-130.5 -53.5z" />
|
||||
<glyph unicode="" d="M1050 495q0 78 -28 147q-41 -25 -85 -34q22 -50 22 -114q0 -117 -77 -198.5t-193 -81.5t-193.5 81.5t-77.5 198.5q0 115 78 199.5t193 84.5q53 0 98 -19q4 43 27 87q-60 21 -125 21q-154 0 -257.5 -108.5t-103.5 -263.5t103.5 -261t257.5 -106t257.5 106.5t103.5 260.5z M872 850q2 -24 2 -71q0 -63 -5 -123t-20.5 -132.5t-40.5 -130t-68.5 -106t-100.5 -70.5q21 -3 42 -3h10q219 139 219 411q0 116 -38 225zM872 850q-4 80 -44 171.5t-98 130.5q92 -156 142 -302zM1207 955q0 102 -51 174q-41 -86 -124 -109q-69 -19 -109 -53.5t-40 -99.5 q0 -40 24 -77q74 17 140.5 67t95.5 115q-4 -52 -74.5 -111.5t-138.5 -97.5q52 -52 110 -52q51 0 90 37t60 90q17 43 17 117zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" />
|
||||
<glyph unicode="" d="M1279 388q0 22 -22 27q-67 15 -118 59t-80 108q-7 19 -7 25q0 15 19.5 26t43 17t43 20.5t19.5 36.5q0 19 -18.5 31.5t-38.5 12.5q-12 0 -32 -8t-31 -8q-4 0 -12 2q5 95 5 114q0 79 -17 114q-36 78 -103 121.5t-152 43.5q-199 0 -275 -165q-17 -35 -17 -114q0 -19 5 -114 q-4 -2 -14 -2q-12 0 -32 7.5t-30 7.5q-21 0 -38.5 -12t-17.5 -32q0 -21 19.5 -35.5t43 -20.5t43 -17t19.5 -26q0 -6 -7 -25q-64 -138 -198 -167q-22 -5 -22 -27q0 -46 137 -68q2 -5 6 -26t11.5 -30.5t23.5 -9.5q12 0 37.5 4.5t39.5 4.5q35 0 67 -15t54 -32.5t57.5 -32.5 t76.5 -15q43 0 79 15t57.5 32.5t53.5 32.5t67 15q14 0 39.5 -4t38.5 -4q16 0 23 10t11 30t6 25q137 22 137 68zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1664" d="M848 1408q134 1 240.5 -68.5t163.5 -192.5q27 -58 27 -179q0 -47 -9 -191q14 -7 28 -7q18 0 51 13.5t51 13.5q29 0 56 -18t27 -46q0 -32 -31.5 -54t-69 -31.5t-69 -29t-31.5 -47.5q0 -15 12 -43q37 -82 102.5 -150t144.5 -101q28 -12 80 -23q28 -6 28 -35 q0 -70 -219 -103q-7 -11 -11 -39t-14 -46.5t-33 -18.5q-20 0 -62 6.5t-64 6.5q-37 0 -62 -5q-32 -5 -63 -22.5t-58 -38t-58 -40.5t-76 -33.5t-99 -13.5q-52 0 -96.5 13.5t-75 33.5t-57.5 40.5t-58 38t-62 22.5q-26 5 -63 5q-24 0 -65.5 -7.5t-58.5 -7.5q-25 0 -35 18.5 t-14 47.5t-11 40q-219 33 -219 103q0 29 28 35q52 11 80 23q78 32 144.5 101t102.5 150q12 28 12 43q0 28 -31.5 47.5t-69.5 29.5t-69.5 31.5t-31.5 52.5q0 27 26 45.5t55 18.5q15 0 48 -13t53 -13q18 0 32 7q-9 142 -9 190q0 122 27 180q64 137 172 198t264 63z" />
|
||||
<glyph unicode="" d="M1280 388q0 22 -22 27q-67 14 -118 58t-80 109q-7 14 -7 25q0 15 19.5 26t42.5 17t42.5 20.5t19.5 36.5q0 19 -18.5 31.5t-38.5 12.5q-11 0 -31 -8t-32 -8q-4 0 -12 2q5 63 5 115q0 78 -17 114q-36 78 -102.5 121.5t-152.5 43.5q-198 0 -275 -165q-18 -38 -18 -115 q0 -38 6 -114q-10 -2 -15 -2q-11 0 -31.5 8t-30.5 8q-20 0 -37.5 -12.5t-17.5 -32.5q0 -21 19.5 -35.5t42.5 -20.5t42.5 -17t19.5 -26q0 -11 -7 -25q-64 -138 -198 -167q-22 -5 -22 -27q0 -47 138 -69q2 -5 6 -26t11 -30.5t23 -9.5q13 0 38.5 5t38.5 5q35 0 67.5 -15 t54.5 -32.5t57.5 -32.5t76.5 -15q43 0 79 15t57.5 32.5t54 32.5t67.5 15q13 0 39 -4.5t39 -4.5q15 0 22.5 9.5t11.5 31t5 24.5q138 22 138 69zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5z" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
<glyph unicode="" horiz-adv-x="1792" />
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 377 KiB |
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Binary file not shown.
@@ -7,11 +7,11 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=all',$(this),'table')" data-success="All History cleared"><i class="fa fa-trash-o"></i> Clear All History</a>
|
||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Processed',$(this),'table')" data-success="All Processed cleared"><i class="fa fa-trash-o"></i> Clear Processed</a>
|
||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Unprocessed',$(this),'table')" data-success="All Unprocessed cleared"><i class="fa fa-trash-o"></i> Clear Unprocessed</a>
|
||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Frozen',$(this),'table')" data-success="All Frozen cleared"><i class="fa fa-trash-o"></i> Clear Frozen</a>
|
||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Snatched',$(this),'table')" data-success="All Snatched cleared"><i class="fa fa-trash-o"></i> Clear Snatched</a>
|
||||
<a id="menu_link_delete" href="javascript:void(0)" onclick="doAjaxCall('clearhistory?type=all',$(this),'table')" data-success="All History cleared"><i class="fa fa-trash-o"></i> Clear All History</a>
|
||||
<a id="menu_link_delete" href="javascript:void(0)" onclick="doAjaxCall('clearhistory?type=Processed',$(this),'table')" data-success="All Processed cleared"><i class="fa fa-trash-o"></i> Clear Processed</a>
|
||||
<a id="menu_link_delete" href="javascript:void(0)" onclick="doAjaxCall('clearhistory?type=Unprocessed',$(this),'table')" data-success="All Unprocessed cleared"><i class="fa fa-trash-o"></i> Clear Unprocessed</a>
|
||||
<a id="menu_link_delete" href="javascript:void(0)" onclick="doAjaxCall('clearhistory?type=Frozen',$(this),'table')" data-success="All Frozen cleared"><i class="fa fa-trash-o"></i> Clear Frozen</a>
|
||||
<a id="menu_link_delete" href="javascript:void(0)" onclick="doAjaxCall('clearhistory?type=Snatched',$(this),'table')" data-success="All Snatched cleared"><i class="fa fa-trash-o"></i> Clear Snatched</a>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
@@ -32,34 +32,41 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
%for item in history:
|
||||
<%
|
||||
if item['Status'] == 'Processed':
|
||||
grade = 'A'
|
||||
elif item['Status'] == 'Snatched':
|
||||
grade = 'C'
|
||||
elif item['Status'] == 'Unprocessed':
|
||||
grade = 'X'
|
||||
elif item['Status'] == 'Frozen':
|
||||
grade = 'X'
|
||||
else:
|
||||
grade = 'U'
|
||||
%for item in history:
|
||||
<%
|
||||
if item['Status'] == 'Processed':
|
||||
grade = 'A'
|
||||
elif item['Status'] == 'Snatched':
|
||||
grade = 'C'
|
||||
elif item['Status'] == 'Unprocessed':
|
||||
grade = 'X'
|
||||
elif item['Status'] == 'Frozen':
|
||||
grade = 'X'
|
||||
else:
|
||||
grade = 'U'
|
||||
|
||||
fileid = 'unknown'
|
||||
if item['URL'].find('nzb') != -1:
|
||||
fileid = 'nzb'
|
||||
if item['URL'].find('torrent') != -1:
|
||||
fileid = 'torrent'
|
||||
if item['URL'].find('rutracker') != -1:
|
||||
fileid = 'torrent'
|
||||
%>
|
||||
fileid = 'unknown'
|
||||
if item['URL'].find('nzb') != -1:
|
||||
fileid = 'nzb'
|
||||
if item['URL'].find('torrent') != -1:
|
||||
fileid = 'torrent'
|
||||
if item['URL'].find('magnet:') != -1:
|
||||
fileid = 'torrent'
|
||||
if item['URL'].find('rutracker') != -1:
|
||||
fileid = 'torrent'
|
||||
if item['URL'].find('codeshy') != -1:
|
||||
fileid = 'nzb'
|
||||
|
||||
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="size">${helpers.bytes_to_mb(item['Size'])}</td>
|
||||
<td id="status">${item['Status']}</td>
|
||||
<td id="action">[<a href="#" 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="#" 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="#" 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 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>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a class="menu_link_edit" id="manage_albums" href="#"><i class="fa fa-pencil"></i> Manage Albums</a>
|
||||
<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">
|
||||
<div class="links">
|
||||
<a href="manageAlbums?Status=Downloaded"><i class="fa fa-check fa-fw"></i> Manage Downloaded Albums</a><br>
|
||||
@@ -90,7 +90,7 @@
|
||||
%>
|
||||
<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="#" 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>
|
||||
<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>
|
||||
</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"/>
|
||||
@@ -121,10 +121,10 @@
|
||||
<fieldset>
|
||||
<legend>Force Search</legend>
|
||||
<div class="links">
|
||||
<a href="#" 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="#" 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="#" 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="#" id="delete_empty_artists"><i class="fa fa-trash-o fa-fw"></i> Delete empty Artists</a>
|
||||
<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">
|
||||
%if emptyArtists:
|
||||
<h3>The following artists will be deleted:</h3>
|
||||
@@ -137,28 +137,27 @@
|
||||
No empty artists found.
|
||||
%endif
|
||||
</div>
|
||||
<a href="#" onclick="doAjaxCall('forcePostProcess',$(this))" data-success="Post-Processor is being loaded" data-error="Error during Post-Processing"><i class="fa fa-wrench fa-fw"></i> Force Post-Process Albums in Download Folder</a>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<form action="forcePostProcess" method="GET">
|
||||
<fieldset>
|
||||
<div class="row">
|
||||
<label>Force Post-Process Albums in Alternate Folder</label>
|
||||
<input type="text" value="" name="dir" id="dir" size="50" /><input type="submit" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
<form action="forcePostProcess" method="GET">
|
||||
<fieldset>
|
||||
<div class="row">
|
||||
<label>Post-Process Single Folder</label>
|
||||
<input type="text" value="" name="album_dir" id="album_dir" size="50" /><input type="submit" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</form>
|
||||
|
||||
<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" />
|
||||
</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" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -167,9 +166,9 @@
|
||||
<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="#" 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>
|
||||
<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>
|
||||
<BR>
|
||||
<a href="#" 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="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>
|
||||
<BR>
|
||||
<small>*Warning: If you choose [Force Re-scan Library], any manually ignored/matched artists/albums will be reset to "unmatched".</small>
|
||||
|
||||
@@ -177,8 +176,7 @@
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dialog-confirm"></div>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
@@ -187,6 +185,55 @@
|
||||
function addScanAction() {
|
||||
$('#autoadd').append('<input type="hidden" name="scan" value=1 />');
|
||||
};
|
||||
|
||||
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() {
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
<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="#" onclick="doAjaxCall('markManual?action=unignoreArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully reset ${album['ArtistName']} to unmatched">Reset Artist</button>
|
||||
<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="#" onclick="doAjaxCall('markManual?action=unmatchArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully restored ${album['ArtistName']} to unmatched">Reset Artist</button>
|
||||
<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>
|
||||
@@ -58,9 +58,9 @@
|
||||
<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="#" 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>
|
||||
<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="#" 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>
|
||||
<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>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<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="#" onclick="doAjaxCall('markUnmatched?action=ignoreArtist&existing_artist=${old_artist_clean}', $(this), 'page');" data-success="Successfully ignored ${album['ArtistName']} from future matching">Ignore Artist</button>
|
||||
<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>
|
||||
@@ -66,7 +66,7 @@
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td></td><td align="right"><BR>
|
||||
<button href="#" onclick="artist_matcher(${count_albums}, '${old_artist_js}')">Match Artist</button>
|
||||
<button href="javascript:void(0)" onclick="artist_matcher(${count_albums}, '${old_artist_js}')">Match Artist</button>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
<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="#" 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>
|
||||
<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>
|
||||
@@ -97,7 +97,7 @@
|
||||
</select>
|
||||
</td></tr>
|
||||
<tr><td></td><td align="right"><BR>
|
||||
<button href="#" onclick="album_matcher(${count_albums}, '${old_artist_js}', '${old_album_js}')">Match Album</button>
|
||||
<button href="javascript:void(0)" onclick="album_matcher(${count_albums}, '${old_artist_js}', '${old_album_js}')">Match Album</button>
|
||||
</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
<th id="reldate">Date</th>
|
||||
<th id="scoresmall">Score</th>
|
||||
<th id="mbrelid" style="display:none;"</th>
|
||||
%else:
|
||||
%elif type == 'artist':
|
||||
<th id="artistname">Artist Name</th>
|
||||
<th id="score">Score</th>
|
||||
%else:
|
||||
<th id="seriesname">Series Name</th>
|
||||
<th id="type">Type</th>
|
||||
<th id="score">Score</th>
|
||||
%endif
|
||||
<th id="mb"></th>
|
||||
@@ -40,8 +44,10 @@
|
||||
<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>
|
||||
%else:
|
||||
%elif type == 'artist':
|
||||
<td id="albumart"><div id="artistImg"><img title="${result['id']}" class="albumArt" height="50" width="50"></div></td>
|
||||
%else:
|
||||
<td id="albumart"></td>
|
||||
%endif
|
||||
%if type == 'album':
|
||||
<td id="albumname"><a href="addReleaseById?rid=${result['albumid']}&rgid=${result['rgid']}" title="${albuminfo}">${result['title']}</a></td>
|
||||
@@ -52,9 +58,14 @@
|
||||
<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>
|
||||
%else:
|
||||
%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>
|
||||
%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>
|
||||
%endif
|
||||
</tr>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a href="#" 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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
+53
-24
@@ -16,20 +16,21 @@
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of
|
||||
# XBian - XBMC on the Raspberry Pi
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import webbrowser
|
||||
import sqlite3
|
||||
import cherrypy
|
||||
import datetime
|
||||
|
||||
import os
|
||||
import cherrypy
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from headphones import versioncheck, logger
|
||||
import headphones.config
|
||||
from headphones.softchroot import SoftChroot
|
||||
import headphones.exceptions
|
||||
|
||||
# (append new extras to the end)
|
||||
POSSIBLE_EXTRAS = [
|
||||
@@ -74,6 +75,7 @@ started = False
|
||||
DATA_DIR = None
|
||||
|
||||
CONFIG = None
|
||||
SOFT_CHROOT = None
|
||||
|
||||
DB_FILE = None
|
||||
|
||||
@@ -85,7 +87,7 @@ LATEST_VERSION = None
|
||||
COMMITS_BEHIND = None
|
||||
|
||||
LOSSY_MEDIA_FORMATS = ["mp3", "aac", "ogg", "ape", "m4a", "asf", "wma"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac"]
|
||||
LOSSLESS_MEDIA_FORMATS = ["flac", "aiff"]
|
||||
MEDIA_FORMATS = LOSSY_MEDIA_FORMATS + LOSSLESS_MEDIA_FORMATS
|
||||
|
||||
MIRRORLIST = ["musicbrainz.org", "headphones", "custom"]
|
||||
@@ -94,10 +96,10 @@ UMASK = None
|
||||
|
||||
|
||||
def initialize(config_file):
|
||||
|
||||
with INIT_LOCK:
|
||||
|
||||
global CONFIG
|
||||
global SOFT_CHROOT
|
||||
global _INITIALIZED
|
||||
global CURRENT_VERSION
|
||||
global LATEST_VERSION
|
||||
@@ -130,12 +132,20 @@ def initialize(config_file):
|
||||
CONFIG.LOG_DIR = None
|
||||
|
||||
if not QUIET:
|
||||
sys.stderr.write("Unable to create the log directory. " \
|
||||
"Logging to screen only.\n")
|
||||
sys.stderr.write("Unable to create the log directory. "
|
||||
"Logging to screen only.\n")
|
||||
|
||||
# Start the logger, disable console if needed
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
verbose=VERBOSE)
|
||||
verbose=VERBOSE)
|
||||
|
||||
try:
|
||||
SOFT_CHROOT = SoftChroot(str(CONFIG.SOFT_CHROOT))
|
||||
if SOFT_CHROOT.isEnabled():
|
||||
logger.info("Soft-chroot enabled for dir: %s", str(CONFIG.SOFT_CHROOT))
|
||||
except headphones.exceptions.SoftChrootError as e:
|
||||
logger.error("SoftChroot error: %s", e)
|
||||
raise e
|
||||
|
||||
if not CONFIG.CACHE_DIR:
|
||||
# Put the cache dir in the data dir for now
|
||||
@@ -176,7 +186,7 @@ def initialize(config_file):
|
||||
version_lock_file, e)
|
||||
|
||||
# Check for new versions
|
||||
if CONFIG.CHECK_GITHUB_ON_STARTUP:
|
||||
if CONFIG.CHECK_GITHUB and CONFIG.CHECK_GITHUB_ON_STARTUP:
|
||||
try:
|
||||
LATEST_VERSION = versioncheck.checkGithub()
|
||||
except:
|
||||
@@ -246,7 +256,6 @@ def daemonize():
|
||||
|
||||
|
||||
def launch_browser(host, port, root):
|
||||
|
||||
if host == '0.0.0.0':
|
||||
host = 'localhost'
|
||||
|
||||
@@ -287,16 +296,19 @@ def initialize_scheduler():
|
||||
hours = CONFIG.UPDATE_DB_INTERVAL
|
||||
schedule_job(updater.dbUpdate, 'MusicBrainz Update', hours=hours, minutes=0)
|
||||
|
||||
#Update check
|
||||
if CONFIG.CHECK_GITHUB_INTERVAL:
|
||||
minutes = CONFIG.CHECK_GITHUB_INTERVAL
|
||||
else:
|
||||
minutes = 0
|
||||
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes)
|
||||
# Update check
|
||||
if CONFIG.CHECK_GITHUB:
|
||||
if CONFIG.CHECK_GITHUB_INTERVAL:
|
||||
minutes = CONFIG.CHECK_GITHUB_INTERVAL
|
||||
else:
|
||||
minutes = 0
|
||||
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0,
|
||||
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)
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
@@ -305,8 +317,8 @@ def initialize_scheduler():
|
||||
except Exception as e:
|
||||
logger.info(e)
|
||||
|
||||
# Debug
|
||||
#SCHED.print_jobs()
|
||||
# Debug
|
||||
# SCHED.print_jobs()
|
||||
|
||||
|
||||
def schedule_job(function, name, hours=0, minutes=0):
|
||||
@@ -333,7 +345,6 @@ def schedule_job(function, name, hours=0, minutes=0):
|
||||
|
||||
|
||||
def start():
|
||||
|
||||
global started
|
||||
|
||||
if _INITIALIZED:
|
||||
@@ -348,16 +359,15 @@ def sig_handler(signum=None, frame=None):
|
||||
|
||||
|
||||
def dbcheck():
|
||||
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT)')
|
||||
'CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT, Extras TEXT, Type TEXT, MetaCritic TEXT)')
|
||||
# ReleaseFormat here means CD,Digital,Vinyl, etc. If using the default
|
||||
# Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is
|
||||
# releasegroup id)
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT, SearchTerm TEXT)')
|
||||
'CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT, SearchTerm TEXT, CriticScore TEXT, UserScore TEXT)')
|
||||
# Format here means mp3, flac, etc.
|
||||
c.execute(
|
||||
'CREATE TABLE IF NOT EXISTS tracks (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)')
|
||||
@@ -583,12 +593,31 @@ def dbcheck():
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE albums ADD COLUMN SearchTerm TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT CriticScore from albums')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE albums ADD COLUMN CriticScore TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT UserScore from albums')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE albums ADD COLUMN UserScore TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT Type from artists')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE artists ADD COLUMN Type TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT MetaCritic from artists')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE artists ADD COLUMN MetaCritic TEXT DEFAULT NULL')
|
||||
|
||||
conn.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False):
|
||||
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from headphones.unittestcompat import TestCase
|
||||
|
||||
import headphones.albumart
|
||||
|
||||
|
||||
class AlbumArtTest(TestCase):
|
||||
def test_nothing(self):
|
||||
x = 100 - 2 * 50
|
||||
if x:
|
||||
headphones.albumart.getAlbumArt('asdf')
|
||||
self.assertTrue(True)
|
||||
@@ -53,7 +53,6 @@ def switch(AlbumID, ReleaseID):
|
||||
c.get_artwork_from_cache(AlbumID=AlbumID)
|
||||
|
||||
for track in newtrackdata:
|
||||
|
||||
controlValueDict = {"TrackID": track['TrackID'],
|
||||
"AlbumID": AlbumID}
|
||||
|
||||
@@ -79,15 +78,18 @@ def switch(AlbumID, ReleaseID):
|
||||
have_track_count = len(myDB.select(
|
||||
'SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID]))
|
||||
|
||||
if oldalbumdata['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
if oldalbumdata['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (
|
||||
headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
myDB.action(
|
||||
'UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
|
||||
|
||||
# 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 WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
||||
[newalbumdata['ArtistID']]))
|
||||
havetracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']]))
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[newalbumdata['ArtistID']]))
|
||||
|
||||
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
|
||||
|
||||
|
||||
+29
-15
@@ -13,21 +13,25 @@
|
||||
# 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 db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, logger
|
||||
|
||||
import headphones
|
||||
import json
|
||||
|
||||
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', 'getSimilar', 'getHistory', 'getLogs',
|
||||
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
|
||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate',
|
||||
'getVersion', 'checkGithub','shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt',
|
||||
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb',
|
||||
from headphones import db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, \
|
||||
logger
|
||||
import headphones
|
||||
|
||||
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched',
|
||||
'getSimilar', 'getHistory', 'getLogs',
|
||||
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist',
|
||||
'refreshArtist',
|
||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess',
|
||||
'forceActiveArtistsUpdate',
|
||||
'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt',
|
||||
'getAlbumArt',
|
||||
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'clearLogs',
|
||||
'choose_specific_download', 'download_specific_release']
|
||||
|
||||
|
||||
class Api(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.apikey = None
|
||||
@@ -170,13 +174,19 @@ class Api(object):
|
||||
self.data = self._dic_from_query(
|
||||
"SELECT * from albums WHERE Status='Snatched'")
|
||||
return
|
||||
|
||||
|
||||
def _getSimilar(self, **kwargs):
|
||||
self.data = self._dic_from_query('SELECT * from lastfmcloud')
|
||||
return
|
||||
|
||||
def _getLogs(self, **kwargs):
|
||||
pass
|
||||
self.data = headphones.LOG_LIST
|
||||
return
|
||||
|
||||
def _clearLogs(self, **kwargs):
|
||||
headphones.LOG_LIST = []
|
||||
self.data = 'Cleared log'
|
||||
return
|
||||
|
||||
def _findArtist(self, **kwargs):
|
||||
if 'name' not in kwargs:
|
||||
@@ -322,10 +332,15 @@ class Api(object):
|
||||
searcher.searchforalbum()
|
||||
|
||||
def _forceProcess(self, **kwargs):
|
||||
self.dir = None
|
||||
if 'dir' in kwargs:
|
||||
if 'album_dir' in kwargs:
|
||||
album_dir = kwargs['album_dir']
|
||||
dir = None
|
||||
postprocessor.forcePostProcess(self, dir, album_dir)
|
||||
elif 'dir' in kwargs:
|
||||
self.dir = kwargs['dir']
|
||||
postprocessor.forcePostProcess(self.dir)
|
||||
postprocessor.forcePostProcess(self.dir)
|
||||
else:
|
||||
postprocessor.forcePostProcess()
|
||||
|
||||
def _forceActiveArtistsUpdate(self, **kwargs):
|
||||
updater.dbUpdate()
|
||||
@@ -426,7 +441,6 @@ class Api(object):
|
||||
results_as_dicts = []
|
||||
|
||||
for result in results:
|
||||
|
||||
result_dict = {
|
||||
'title': result[0],
|
||||
'size': result[1],
|
||||
|
||||
+34
-17
@@ -14,8 +14,8 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import headphones
|
||||
|
||||
import headphones
|
||||
from headphones import db, helpers, logger, lastfm, request
|
||||
|
||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
||||
@@ -45,8 +45,8 @@ class Cache(object):
|
||||
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
|
||||
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
|
||||
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
|
||||
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
|
||||
|
||||
self.artwork_files = []
|
||||
self.thumb_files = []
|
||||
@@ -116,7 +116,7 @@ class Cache(object):
|
||||
return None
|
||||
|
||||
for image in images:
|
||||
if image['size'] == 'medium':
|
||||
if image['size'] == 'medium' and '#text' in image:
|
||||
thumb_url = image['#text']
|
||||
break
|
||||
|
||||
@@ -182,13 +182,18 @@ class Cache(object):
|
||||
if ArtistID:
|
||||
self.id = ArtistID
|
||||
self.id_type = 'artist'
|
||||
db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?', [self.id]).fetchone()
|
||||
db_info = myDB.action(
|
||||
'SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?',
|
||||
[self.id]).fetchone()
|
||||
else:
|
||||
self.id = AlbumID
|
||||
self.id_type = 'album'
|
||||
db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?', [self.id]).fetchone()
|
||||
db_info = myDB.action(
|
||||
'SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?',
|
||||
[self.id]).fetchone()
|
||||
|
||||
if not db_info or not db_info['LastUpdated'] or not self._is_current(date=db_info['LastUpdated']):
|
||||
if not db_info or not db_info['LastUpdated'] or not self._is_current(
|
||||
date=db_info['LastUpdated']):
|
||||
|
||||
self._update_cache()
|
||||
info_dict = {'Summary': self.info_summary, 'Content': self.info_content}
|
||||
@@ -309,13 +314,19 @@ class Cache(object):
|
||||
logger.debug('No artist thumbnail image found')
|
||||
|
||||
else:
|
||||
dbalbum = myDB.action('SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', [self.id]).fetchone()
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID 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)
|
||||
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'], api_key=LASTFM_API_KEY)
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=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)
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
@@ -357,7 +368,8 @@ class Cache(object):
|
||||
# Save the image URL to the database
|
||||
if image_url:
|
||||
if self.id_type == 'artist':
|
||||
myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?', [image_url, self.id])
|
||||
myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?',
|
||||
[image_url, self.id])
|
||||
else:
|
||||
myDB.action('UPDATE albums SET ArtworkURL=? WHERE AlbumID=?', [image_url, self.id])
|
||||
|
||||
@@ -378,7 +390,8 @@ class Cache(object):
|
||||
if not os.path.isdir(self.path_to_art_cache):
|
||||
try:
|
||||
os.makedirs(self.path_to_art_cache)
|
||||
os.chmod(self.path_to_art_cache, int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
os.chmod(self.path_to_art_cache,
|
||||
int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except OSError as e:
|
||||
logger.error('Unable to create artwork cache dir. Error: %s', e)
|
||||
self.artwork_errors = True
|
||||
@@ -393,7 +406,8 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext)
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(artwork_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
@@ -406,7 +420,8 @@ class Cache(object):
|
||||
|
||||
# Grab the thumbnail as well if we're getting the full artwork (as long
|
||||
# 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])):
|
||||
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 artwork:
|
||||
@@ -414,7 +429,8 @@ class Cache(object):
|
||||
if not os.path.isdir(self.path_to_art_cache):
|
||||
try:
|
||||
os.makedirs(self.path_to_art_cache)
|
||||
os.chmod(self.path_to_art_cache, int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
os.chmod(self.path_to_art_cache,
|
||||
int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except OSError as e:
|
||||
logger.error('Unable to create artwork cache dir. Error: %s' + e)
|
||||
self.thumb_errors = True
|
||||
@@ -429,7 +445,8 @@ 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)
|
||||
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)
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#########################################
|
||||
## Stolen from Sick-Beard's classes.py ##
|
||||
#########################################
|
||||
#######################################
|
||||
# Stolen from Sick-Beard's classes.py #
|
||||
#######################################
|
||||
|
||||
|
||||
import urllib
|
||||
@@ -133,4 +133,5 @@ class Proper:
|
||||
self.episode = -1
|
||||
|
||||
def __str__(self):
|
||||
return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(self.episode) + " of " + str(self.tvdbid)
|
||||
return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(
|
||||
self.episode) + " of " + str(self.tvdbid)
|
||||
|
||||
+30
-26
@@ -20,15 +20,16 @@ Created on Aug 1, 2011
|
||||
'''
|
||||
import platform
|
||||
import operator
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from headphones import version
|
||||
|
||||
#Identify Our Application
|
||||
|
||||
# Identify Our Application
|
||||
USER_AGENT = 'Headphones/-' + version.HEADPHONES_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
|
||||
|
||||
### Notification Types
|
||||
# Notification Types
|
||||
NOTIFY_SNATCH = 1
|
||||
NOTIFY_DOWNLOAD = 2
|
||||
|
||||
@@ -36,26 +37,25 @@ notifyStrings = {}
|
||||
notifyStrings[NOTIFY_SNATCH] = "Started Download"
|
||||
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
|
||||
|
||||
### Release statuses
|
||||
UNKNOWN = -1 # should never happen
|
||||
UNAIRED = 1 # releases that haven't dropped yet
|
||||
SNATCHED = 2 # qualified with quality
|
||||
WANTED = 3 # releases we don't have but want to get
|
||||
DOWNLOADED = 4 # qualified with quality
|
||||
SKIPPED = 5 # releases we don't want
|
||||
ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
|
||||
IGNORED = 7 # releases that you don't want included in your download stats
|
||||
SNATCHED_PROPER = 9 # qualified with quality
|
||||
# Release statuses
|
||||
UNKNOWN = -1 # should never happen
|
||||
UNAIRED = 1 # releases that haven't dropped yet
|
||||
SNATCHED = 2 # qualified with quality
|
||||
WANTED = 3 # releases we don't have but want to get
|
||||
DOWNLOADED = 4 # qualified with quality
|
||||
SKIPPED = 5 # releases we don't want
|
||||
ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
|
||||
IGNORED = 7 # releases that you don't want included in your download stats
|
||||
SNATCHED_PROPER = 9 # qualified with quality
|
||||
|
||||
|
||||
class Quality:
|
||||
|
||||
NONE = 0
|
||||
B192 = 1 << 1 # 2
|
||||
VBR = 1 << 2 # 4
|
||||
B256 = 1 << 3 # 8
|
||||
B320 = 1 << 4 #16
|
||||
FLAC = 1 << 5 #32
|
||||
B192 = 1 << 1 # 2
|
||||
VBR = 1 << 2 # 4
|
||||
B256 = 1 << 3 # 8
|
||||
B320 = 1 << 4 # 16
|
||||
FLAC = 1 << 5 # 32
|
||||
|
||||
# put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
|
||||
UNKNOWN = 1 << 15
|
||||
@@ -75,7 +75,8 @@ class Quality:
|
||||
def _getStatusStrings(status):
|
||||
toReturn = {}
|
||||
for x in Quality.qualityStrings.keys():
|
||||
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + Quality.qualityStrings[x] + ")"
|
||||
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \
|
||||
Quality.qualityStrings[x] + ")"
|
||||
return toReturn
|
||||
|
||||
@staticmethod
|
||||
@@ -103,6 +104,9 @@ class Quality:
|
||||
@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
|
||||
@@ -115,9 +119,7 @@ class Quality:
|
||||
if regex_match:
|
||||
return x
|
||||
|
||||
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
|
||||
|
||||
#TODO: fix quality checking here
|
||||
# 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):
|
||||
@@ -131,7 +133,6 @@ class Quality:
|
||||
|
||||
@staticmethod
|
||||
def assumeQuality(name):
|
||||
|
||||
if name.lower().endswith(".mp3"):
|
||||
return Quality.MP3
|
||||
elif name.lower().endswith(".flac"):
|
||||
@@ -167,13 +168,16 @@ class Quality:
|
||||
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.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
|
||||
Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in
|
||||
Quality.qualityStrings.keys()]
|
||||
|
||||
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
|
||||
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
|
||||
ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
|
||||
ANY = Quality.combineQualities(
|
||||
[Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
|
||||
|
||||
qualityPresets = (MP3, LOSSLESS, ANY)
|
||||
qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
|
||||
|
||||
+114
-77
@@ -1,7 +1,8 @@
|
||||
import headphones.logger
|
||||
import itertools
|
||||
|
||||
import os
|
||||
import re
|
||||
import headphones.logger
|
||||
from configobj import ConfigObj
|
||||
|
||||
|
||||
@@ -14,6 +15,21 @@ def bool_int(value):
|
||||
value = 0
|
||||
return int(bool(value))
|
||||
|
||||
|
||||
class path(str):
|
||||
"""Internal 'marker' type for paths in config."""
|
||||
|
||||
@staticmethod
|
||||
def __call__(val):
|
||||
return path(val)
|
||||
|
||||
def __new__(cls, *args, **kw):
|
||||
hstr = str.__new__(cls, *args, **kw)
|
||||
return hstr
|
||||
|
||||
def __repr__(self):
|
||||
return 'headphones.config.path(%s)' % self
|
||||
|
||||
_CONFIG_DEFINITIONS = {
|
||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||
'ADVANCEDENCODER': (str, 'General', ''),
|
||||
@@ -29,53 +45,76 @@ _CONFIG_DEFINITIONS = {
|
||||
'AUTO_ADD_ARTISTS': (int, 'General', 1),
|
||||
'BITRATE': (int, 'General', 192),
|
||||
'BLACKHOLE': (int, 'General', 0),
|
||||
'BLACKHOLE_DIR': (str, 'General', ''),
|
||||
'BLACKHOLE_DIR': (path, 'General', ''),
|
||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
||||
'BOXCAR_ONSNATCH': (int, 'Boxcar', 0),
|
||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
||||
'CACHE_DIR': (str, 'General', ''),
|
||||
'CACHE_DIR': (path, 'General', ''),
|
||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||
'CHECK_GITHUB': (int, 'General', 1),
|
||||
'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
|
||||
'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
|
||||
'CLEANUP_FILES': (int, 'General', 0),
|
||||
'CONFIG_VERSION': (str, 'General', '0'),
|
||||
'CONFIG_VERSION': (str, 'General', '2'),
|
||||
'CORRECT_METADATA': (int, 'General', 0),
|
||||
'CUE_SPLIT': (int, 'General', 1),
|
||||
'CUE_SPLIT_FLAC_PATH': (str, 'General', ''),
|
||||
'CUE_SPLIT_SHNTOOL_PATH': (str, 'General', ''),
|
||||
'CUE_SPLIT_FLAC_PATH': (path, 'General', ''),
|
||||
'CUE_SPLIT_SHNTOOL_PATH': (path, 'General', ''),
|
||||
'CUSTOMAUTH': (int, 'General', 0),
|
||||
'CUSTOMHOST': (str, 'General', 'localhost'),
|
||||
'CUSTOMPASS': (str, 'General', ''),
|
||||
'CUSTOMPORT': (int, 'General', 5000),
|
||||
'CUSTOMSLEEP': (int, 'General', 1),
|
||||
'CUSTOMUSER': (str, 'General', ''),
|
||||
'DELETE_LOSSLESS_FILES': (int, 'General', 1),
|
||||
'DELUGE_HOST': (str, 'Deluge', ''),
|
||||
'DELUGE_CERT': (str, 'Deluge', ''),
|
||||
'DELUGE_PASSWORD': (str, 'Deluge', ''),
|
||||
'DELUGE_LABEL': (str, 'Deluge', ''),
|
||||
'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
|
||||
'DELUGE_PAUSED': (int, 'Deluge', 0),
|
||||
'DESTINATION_DIR': (str, 'General', ''),
|
||||
'DETECT_BITRATE': (int, 'General', 0),
|
||||
'DOWNLOAD_DIR': (str, 'General', ''),
|
||||
'DO_NOT_PROCESS_UNMATCHED': (int, 'General', 0),
|
||||
'DOWNLOAD_DIR': (path, 'General', ''),
|
||||
'DOWNLOAD_SCAN_INTERVAL': (int, 'General', 5),
|
||||
'DOWNLOAD_TORRENT_DIR': (str, 'General', ''),
|
||||
'DOWNLOAD_TORRENT_DIR': (path, 'General', ''),
|
||||
'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
|
||||
'EMAIL_ENABLED': (int, 'Email', 0),
|
||||
'EMAIL_FROM': (str, 'Email', ''),
|
||||
'EMAIL_TO': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_SERVER': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_USER': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_PASSWORD': (str, 'Email', ''),
|
||||
'EMAIL_SMTP_PORT': (int, 'Email', 25),
|
||||
'EMAIL_SSL': (int, 'Email', 0),
|
||||
'EMAIL_TLS': (int, 'Email', 0),
|
||||
'EMAIL_ONSNATCH': (int, 'Email', 0),
|
||||
'EMBED_ALBUM_ART': (int, 'General', 0),
|
||||
'EMBED_LYRICS': (int, 'General', 0),
|
||||
'ENABLE_HTTPS': (int, 'General', 0),
|
||||
'ENCODER': (str, 'General', 'ffmpeg'),
|
||||
'ENCODERFOLDER': (str, 'General', ''),
|
||||
'ENCODERFOLDER': (path, 'General', ''),
|
||||
'ENCODERLOSSLESS': (int, 'General', 1),
|
||||
'ENCODEROUTPUTFORMAT': (str, 'General', 'mp3'),
|
||||
'ENCODERQUALITY': (int, 'General', 2),
|
||||
'ENCODERVBRCBR': (str, 'General', 'cbr'),
|
||||
'ENCODER_MULTICORE': (int, 'General', 0),
|
||||
'ENCODER_MULTICORE_COUNT': (int, 'General', 0),
|
||||
'ENCODER_PATH': (str, 'General', ''),
|
||||
'ENCODER_PATH': (path, 'General', ''),
|
||||
'EXTRAS': (str, 'General', ''),
|
||||
'EXTRA_NEWZNABS': (list, 'Newznab', ''),
|
||||
'FILE_FORMAT': (str, 'General', 'Track Artist - Album [Year] - Title'),
|
||||
'EXTRA_TORZNABS': (list, 'Torznab', ''),
|
||||
'FILE_FORMAT': (str, 'General', '$Track $Artist - $Album [$Year] - $Title'),
|
||||
'FILE_PERMISSIONS': (str, 'General', '0644'),
|
||||
'FILE_PERMISSIONS_ENABLED': (bool_int, 'General', True),
|
||||
'FILE_UNDERSCORES': (int, 'General', 0),
|
||||
'FOLDER_FORMAT': (str, 'General', 'Artist/Album [Year]'),
|
||||
'FOLDER_FORMAT': (str, 'General', '$Artist/$Album [$Year]'),
|
||||
'FOLDER_PERMISSIONS_ENABLED': (bool_int, 'General', True),
|
||||
'FOLDER_PERMISSIONS': (str, 'General', '0755'),
|
||||
'FREEZE_DB': (int, 'General', 0),
|
||||
'GIT_BRANCH': (str, 'General', 'master'),
|
||||
'GIT_PATH': (str, 'General', ''),
|
||||
'GIT_PATH': (path, 'General', ''),
|
||||
'GIT_USER': (str, 'General', 'rembo10'),
|
||||
'GROWL_ENABLED': (int, 'Growl', 0),
|
||||
'GROWL_HOST': (str, 'Growl', ''),
|
||||
@@ -84,17 +123,19 @@ _CONFIG_DEFINITIONS = {
|
||||
'HEADPHONES_INDEXER': (bool_int, 'General', False),
|
||||
'HPPASS': (str, 'General', ''),
|
||||
'HPUSER': (str, 'General', ''),
|
||||
'HTTPS_CERT': (str, 'General', ''),
|
||||
'HTTPS_KEY': (str, 'General', ''),
|
||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||
'HTTPS_CERT': (path, 'General', ''),
|
||||
'HTTPS_KEY': (path, 'General', ''),
|
||||
'HTTP_HOST': (str, 'General', 'localhost'),
|
||||
'HTTP_PASSWORD': (str, 'General', ''),
|
||||
'HTTP_PORT': (int, 'General', 8181),
|
||||
'HTTP_PROXY': (int, 'General', 0),
|
||||
'HTTP_ROOT': (str, 'General', '/'),
|
||||
'HTTP_USERNAME': (str, 'General', ''),
|
||||
'IDTAG': (int, 'Beets', 0),
|
||||
'IGNORE_CLEAN_RELEASES': (int, 'General', 0),
|
||||
'IGNORED_WORDS': (str, 'General', ''),
|
||||
'IGNORED_FOLDERS': (list, 'Advanced', []),
|
||||
'IGNORED_FILES': (list, 'Advanced', []),
|
||||
'IGNORED_FOLDERS': (list, 'Advanced', []), # path
|
||||
'IGNORED_FILES': (list, 'Advanced', []), # path
|
||||
'INCLUDE_EXTRAS': (int, 'General', 0),
|
||||
'INTERFACE': (str, 'General', 'default'),
|
||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||
@@ -109,17 +150,17 @@ _CONFIG_DEFINITIONS = {
|
||||
'LIBRARYSCAN_INTERVAL': (int, 'General', 300),
|
||||
'LMS_ENABLED': (int, 'LMS', 0),
|
||||
'LMS_HOST': (str, 'LMS', ''),
|
||||
'LOG_DIR': (str, 'General', ''),
|
||||
'LOG_DIR': (path, 'General', ''),
|
||||
'LOSSLESS_BITRATE_FROM': (int, 'General', 0),
|
||||
'LOSSLESS_BITRATE_TO': (int, 'General', 0),
|
||||
'LOSSLESS_DESTINATION_DIR': (str, 'General', ''),
|
||||
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
||||
'MB_IGNORE_AGE': (int, 'General', 365),
|
||||
'MININOVA': (int, 'Mininova', 0),
|
||||
'MININOVA_RATIO': (str, 'Mininova', ''),
|
||||
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
||||
'MOVE_FILES': (int, 'General', 0),
|
||||
'MPC_ENABLED': (bool_int, 'MPC', False),
|
||||
'MUSIC_DIR': (str, 'General', ''),
|
||||
'MUSIC_DIR': (path, 'General', ''),
|
||||
'MUSIC_ENCODER': (int, 'General', 0),
|
||||
'NEWZNAB': (int, 'Newznab', 0),
|
||||
'NEWZNAB_APIKEY': (str, 'Newznab', ''),
|
||||
@@ -139,10 +180,11 @@ _CONFIG_DEFINITIONS = {
|
||||
'NZBSORG_HASH': (str, 'NZBsorg', ''),
|
||||
'NZBSORG_UID': (str, 'NZBsorg', ''),
|
||||
'NZB_DOWNLOADER': (int, 'General', 0),
|
||||
'OFFICIAL_RELEASES_ONLY': (int, 'General', 0),
|
||||
'OMGWTFNZBS': (int, 'omgwtfnzbs', 0),
|
||||
'OMGWTFNZBS_APIKEY': (str, 'omgwtfnzbs', ''),
|
||||
'OMGWTFNZBS_UID': (str, 'omgwtfnzbs', ''),
|
||||
'OPEN_MAGNET_LINKS': (int, 'General', 0), # 0: Ignore, 1: Open, 2: Convert
|
||||
'OPEN_MAGNET_LINKS': (int, 'General', 0), # 0: Ignore, 1: Open, 2: Convert, 3: Embed (rtorrent)
|
||||
'MAGNET_LINKS': (int, 'General', 0),
|
||||
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/Headphones'),
|
||||
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
||||
@@ -160,6 +202,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PLEX_SERVER_HOST': (str, 'Plex', ''),
|
||||
'PLEX_UPDATE': (int, 'Plex', 0),
|
||||
'PLEX_USERNAME': (str, 'Plex', ''),
|
||||
'PLEX_TOKEN': (str, 'Plex', ''),
|
||||
'PREFERRED_BITRATE': (str, 'General', ''),
|
||||
'PREFERRED_BITRATE_ALLOW_LOSSLESS': (int, 'General', 0),
|
||||
'PREFERRED_BITRATE_HIGH_BUFFER': (int, 'General', 0),
|
||||
@@ -184,7 +227,10 @@ _CONFIG_DEFINITIONS = {
|
||||
'PUSHOVER_ONSNATCH': (int, 'Pushover', 0),
|
||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||
'RENAME_FILES': (int, 'General', 0),
|
||||
'RENAME_UNPROCESSED': (bool_int, 'General', 1),
|
||||
'RENAME_FROZEN': (bool_int, 'General', 1),
|
||||
'REPLACE_EXISTING_FOLDERS': (int, 'General', 0),
|
||||
'KEEP_ORIGINAL_FOLDER': (int, 'General', 0),
|
||||
'REQUIRED_WORDS': (str, 'General', ''),
|
||||
'RUTRACKER': (int, 'Rutracker', 0),
|
||||
'RUTRACKER_PASSWORD': (str, 'Rutracker', ''),
|
||||
@@ -197,18 +243,29 @@ _CONFIG_DEFINITIONS = {
|
||||
'SAB_USERNAME': (str, 'SABnzbd', ''),
|
||||
'SAMPLINGFREQUENCY': (int, 'General', 44100),
|
||||
'SEARCH_INTERVAL': (int, 'General', 1440),
|
||||
'SOFT_CHROOT': (path, 'General', ''),
|
||||
'SONGKICK_APIKEY': (str, 'Songkick', 'nd1We7dFW2RqxPw8'),
|
||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
||||
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
||||
'STRIKE': (int, 'Strike', 0),
|
||||
'STRIKE_RATIO': (str, 'Strike', ''),
|
||||
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
||||
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
||||
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
||||
'SUBSONIC_USERNAME': (str, 'Subsonic', ''),
|
||||
'SYNOINDEX_ENABLED': (int, 'Synoindex', 0),
|
||||
'TELEGRAM_TOKEN': (str, 'Telegram', ''),
|
||||
'TELEGRAM_USERID': (str, 'Telegram', ''),
|
||||
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ONSNATCH': (int, 'Telegram', 0),
|
||||
'TORRENTBLACKHOLE_DIR': (str, 'General', ''),
|
||||
'TORRENT_DOWNLOADER': (int, 'General', 0),
|
||||
'TORRENT_REMOVAL_INTERVAL': (int, 'General', 720),
|
||||
'TORZNAB': (int, 'Torznab', 0),
|
||||
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
||||
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
||||
'TORZNAB_HOST': (str, 'Torznab', ''),
|
||||
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
||||
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
||||
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
||||
@@ -217,6 +274,9 @@ _CONFIG_DEFINITIONS = {
|
||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
||||
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
|
||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
||||
'TQUATTRECENTONZE': (int, 'tquattrecentonze', 0),
|
||||
'TQUATTRECENTONZE_PASSWORD': (str, 'tquattrecentonze', ''),
|
||||
'TQUATTRECENTONZE_USER': (str, 'tquattrecentonze', ''),
|
||||
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
||||
'USENET_RETENTION': (int, 'General', '1500'),
|
||||
'UTORRENT_HOST': (str, 'uTorrent', ''),
|
||||
@@ -224,6 +284,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'UTORRENT_PASSWORD': (str, 'uTorrent', ''),
|
||||
'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', ''),
|
||||
@@ -232,6 +293,12 @@ _CONFIG_DEFINITIONS = {
|
||||
'WHATCD_PASSWORD': (str, 'What.cd', ''),
|
||||
'WHATCD_RATIO': (str, 'What.cd', ''),
|
||||
'WHATCD_USERNAME': (str, 'What.cd', ''),
|
||||
'WHATCD_URL': (str, 'What.cd', 'https://what.cd'),
|
||||
'PTH': (int, 'PassTheHeadphones.me', 0),
|
||||
'PTH_PASSWORD': (str, 'PassTheHeadphones.me', ''),
|
||||
'PTH_RATIO': (str, 'PassTheHeadphones.me', ''),
|
||||
'PTH_USERNAME': (str, 'PassTheHeadphones.me', ''),
|
||||
'PTH_URL': (str, 'PassTheHeadphones.me', 'https://passtheheadphones.me'),
|
||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||
'XBMC_HOST': (str, 'XBMC', ''),
|
||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||
@@ -241,6 +308,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'XLDPROFILE': (str, 'General', '')
|
||||
}
|
||||
|
||||
|
||||
# pylint:disable=R0902
|
||||
# it might be nice to refactor for fewer instance variables
|
||||
class Config(object):
|
||||
@@ -261,7 +329,7 @@ class Config(object):
|
||||
definition = _CONFIG_DEFINITIONS[key]
|
||||
if len(definition) == 3:
|
||||
definition_type, section, default = definition
|
||||
else:
|
||||
elif len(definition) == 4:
|
||||
definition_type, section, _, default = definition
|
||||
return key, definition_type, section, ini_key, default
|
||||
|
||||
@@ -317,7 +385,7 @@ class Config(object):
|
||||
""" Return the extra newznab tuples """
|
||||
extra_newznabs = list(
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_NEWZNABS, i, None, 3)
|
||||
for i in range(3)])
|
||||
for i in range(3)])
|
||||
)
|
||||
return extra_newznabs
|
||||
|
||||
@@ -332,6 +400,25 @@ class Config(object):
|
||||
extra_newznabs.append(item)
|
||||
self.EXTRA_NEWZNABS = extra_newznabs
|
||||
|
||||
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)])
|
||||
)
|
||||
return extra_torznabs
|
||||
|
||||
def clear_extra_torznabs(self):
|
||||
""" Forget about the configured extra torznabs """
|
||||
self.EXTRA_TORZNABS = []
|
||||
|
||||
def add_extra_torznab(self, torznab):
|
||||
""" Add a new extra torznab """
|
||||
extra_torznabs = self.EXTRA_TORZNABS
|
||||
for item in torznab:
|
||||
extra_torznabs.append(item)
|
||||
self.EXTRA_TORZNABS = extra_torznabs
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Returns something from the ini unless it is a real property
|
||||
@@ -364,59 +451,9 @@ class Config(object):
|
||||
self._config[section][ini_key] = definition_type(value)
|
||||
|
||||
def _upgrade(self):
|
||||
""" Update folder formats in the config & bump up config version """
|
||||
if self.CONFIG_VERSION == '0':
|
||||
from headphones.helpers import replace_all
|
||||
file_values = {
|
||||
'tracknumber': 'Track',
|
||||
'title': 'Title',
|
||||
'artist': 'Artist',
|
||||
'album': 'Album',
|
||||
'year': 'Year'
|
||||
}
|
||||
folder_values = {
|
||||
'artist': 'Artist',
|
||||
'album': 'Album',
|
||||
'year': 'Year',
|
||||
'releasetype': 'Type',
|
||||
'first': 'First',
|
||||
'lowerfirst': 'first'
|
||||
}
|
||||
self.FILE_FORMAT = replace_all(self.FILE_FORMAT, file_values)
|
||||
self.FOLDER_FORMAT = replace_all(self.FOLDER_FORMAT, folder_values)
|
||||
|
||||
self.CONFIG_VERSION = '1'
|
||||
|
||||
if self.CONFIG_VERSION == '1':
|
||||
from headphones.helpers import replace_all
|
||||
file_values = {
|
||||
'Track': '$Track',
|
||||
'Title': '$Title',
|
||||
'Artist': '$Artist',
|
||||
'Album': '$Album',
|
||||
'Year': '$Year',
|
||||
'track': '$track',
|
||||
'title': '$title',
|
||||
'artist': '$artist',
|
||||
'album': '$album',
|
||||
'year': '$year'
|
||||
}
|
||||
folder_values = {
|
||||
'Artist': '$Artist',
|
||||
'Album': '$Album',
|
||||
'Year': '$Year',
|
||||
'Type': '$Type',
|
||||
'First': '$First',
|
||||
'artist': '$artist',
|
||||
'album': '$album',
|
||||
'year': '$year',
|
||||
'type': '$type',
|
||||
'first': '$first'
|
||||
}
|
||||
self.FILE_FORMAT = replace_all(self.FILE_FORMAT, file_values)
|
||||
self.FOLDER_FORMAT = replace_all(self.FOLDER_FORMAT, folder_values)
|
||||
self.CONFIG_VERSION = '2'
|
||||
|
||||
"""
|
||||
Bring old configs up to date
|
||||
"""
|
||||
if self.CONFIG_VERSION == '2':
|
||||
# Update the config to use direct path to the encoder rather than the encoder folder
|
||||
if self.ENCODERFOLDER:
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
import mock
|
||||
from mock import MagicMock
|
||||
import headphones.config
|
||||
import re
|
||||
import unittestcompat
|
||||
from unittestcompat import TestCase, TestArgs
|
||||
|
||||
|
||||
class ConfigApiTest(TestCase):
|
||||
""" Common tests for headphones.Config
|
||||
|
||||
Common tests for headphones.Config This test suite guarantees, that external
|
||||
API of the Config class conforms all expectations of other modules.
|
||||
"""
|
||||
|
||||
def _setUpConfigMock(self, mock, sections):
|
||||
# every constructor `xx = ConfigObj()` in headphones.config will return
|
||||
# this mock:
|
||||
self.config_mock = self.config_module_mock.return_value = mock
|
||||
|
||||
if sections:
|
||||
mock.__contains__.side_effect = sections.__contains__
|
||||
mock.__getitem__.side_effect = sections.__getitem__
|
||||
mock.__setitem__.side_effect = sections.__setitem__
|
||||
mock.items.side_effect = sections.items
|
||||
|
||||
return mock
|
||||
|
||||
def setUp(self):
|
||||
# patch for low-level ConfigObj for entire test class
|
||||
# result - each test_* method will get one additional
|
||||
# argument during testing
|
||||
self.config_module_mock_patcher = mock.patch('headphones.config.ConfigObj', name='ConfigObjModuleMock')
|
||||
self.config_module_mock = self.config_module_mock_patcher.start()
|
||||
|
||||
existing_sections = {'General': {}, 'Email': {}}
|
||||
# every constructor `xx = ConfigObj()` in headphones.config will return
|
||||
# this mock:
|
||||
self._setUpConfigMock(MagicMock(), existing_sections)
|
||||
|
||||
def tearDown(self):
|
||||
self.config_module_mock_patcher.stop()
|
||||
|
||||
def test_constructor(self):
|
||||
""" Config : creating """
|
||||
|
||||
cf = headphones.config.Config('/tmp/notexist')
|
||||
self.assertIsInstance(cf, headphones.config.Config)
|
||||
|
||||
@TestArgs(
|
||||
# this sections are explicitly added in the test body:
|
||||
('General', False),
|
||||
('Email', False),
|
||||
|
||||
# this sections will not be created nor in the test, either in the
|
||||
# Config module
|
||||
('some_new_section_never_defined', True),
|
||||
('another_new_section_never_defined', True),
|
||||
)
|
||||
def test_check_section(self, section_name, expected_return):
|
||||
""" Config : check_section """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.check_section(section_name)
|
||||
res2 = c.check_section(section_name)
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(res, expected_return)
|
||||
self.assertFalse(res2)
|
||||
|
||||
@TestArgs(
|
||||
('api_enabled', 0, int),
|
||||
('Api_Key', '', str),
|
||||
)
|
||||
def test_check_setting(self, setting_name, expected_return, expected_instance):
|
||||
""" Config: check_setting , basic cases """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.check_setting(setting_name)
|
||||
res2 = c.check_setting(setting_name)
|
||||
|
||||
# assertions:
|
||||
self.assertIsInstance(res, expected_instance)
|
||||
self.assertEqual(res, expected_return)
|
||||
self.assertEqual(res, res2)
|
||||
|
||||
@TestArgs(
|
||||
(''),
|
||||
('This_IsNew_Name'),
|
||||
)
|
||||
def test_check_setting_raise_on_unknown_settings(self, setting_name):
|
||||
""" Config: check_setting should raise on unknown """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
exc_regex = re.compile(setting_name, re.IGNORECASE)
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
# assertions:
|
||||
with self.assertRaisesRegexp(KeyError, exc_regex):
|
||||
c.check_setting(setting_name)
|
||||
pass
|
||||
|
||||
@TestArgs(
|
||||
(None)
|
||||
)
|
||||
def test_check_setting_raise_on_none(self, setting_name):
|
||||
""" Config: check_setting shoud raise on None name """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
# assertions:
|
||||
with self.assertRaises(AttributeError):
|
||||
c.check_setting(setting_name)
|
||||
pass
|
||||
|
||||
def test_write(self):
|
||||
""" Config : write """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# overload mocks, defined in setUp:
|
||||
old_conf_mock = self._setUpConfigMock(MagicMock(), {'a': {}})
|
||||
|
||||
option_name_not_from_definitions = 'some_invalid_option_with_super_uniq1_name'
|
||||
option_name_not_from_definitions_value = 1
|
||||
old_conf_mock['asdf'] = {option_name_not_from_definitions: option_name_not_from_definitions_value}
|
||||
|
||||
# call methods
|
||||
cf = headphones.config.Config(path)
|
||||
|
||||
# overload mock-patching for NEW CONFIG
|
||||
new_patcher = mock.patch('headphones.config.ConfigObj', name='NEW_ConfigObjModuleMock_FOR_WRITE')
|
||||
|
||||
new_conf_module_mock = new_patcher.start()
|
||||
new_conf_mock = \
|
||||
new_conf_module_mock.return_value = \
|
||||
MagicMock()
|
||||
cf.write()
|
||||
new_patcher.stop()
|
||||
|
||||
# assertions:
|
||||
self.assertFalse(old_conf_mock.write.called, 'write not called for old config')
|
||||
self.assertTrue(new_conf_mock.write.called, 'write called for new config')
|
||||
self.assertEqual(new_conf_mock.filename, path)
|
||||
|
||||
new_conf_mock['General'].__setitem__.assert_any_call('download_dir', '')
|
||||
# from 3.5... new_conf_mock['asdf'].__setitem__.assert_not_called('download_dir', '')
|
||||
new_conf_mock['asdf'].__setitem__.assert_any_call(option_name_not_from_definitions, option_name_not_from_definitions_value)
|
||||
|
||||
@unittestcompat.skip("process_kwargs should be removed")
|
||||
def test_process_kwargs(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
# ===========================================================
|
||||
# GET ATTR
|
||||
# ===========================================================
|
||||
|
||||
@TestArgs(
|
||||
('ADD_ALBUM_ART', True),
|
||||
('ALBUM_ART_FORMAT', 'shmolder'),
|
||||
('API_ENABLED', 1),
|
||||
('API_KEY', 'Hello'),
|
||||
)
|
||||
def test__getattr__ConfValues(self, name, value):
|
||||
""" Config: __getattr__ with setting value explicit """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["General"] = {name.lower(): value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
act = c.__getattr__(name)
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(act, value)
|
||||
|
||||
@TestArgs(
|
||||
('ADD_ALBUM_ART', 0),
|
||||
('ALBUM_ART_FORMAT', 'folder'),
|
||||
('API_ENABLED', 0),
|
||||
('API_KEY', ''),
|
||||
)
|
||||
def test__getattr__ConfValuesDefault(self, name, value):
|
||||
""" Config: __getattr__ from config(by braces), default values """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.__getattr__(name)
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(res, value)
|
||||
|
||||
def test__getattr__ConfValuesDefaultUsingDotNotation(self):
|
||||
""" Config: __getattr__ from config (by dot), default values """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(c.ALBUM_ART_FORMAT, 'folder')
|
||||
self.assertEqual(c.API_ENABLED, 0)
|
||||
self.assertEqual(c.API_KEY, '')
|
||||
|
||||
def test__getattr__OwnAttributes(self):
|
||||
""" Config: __getattr__ access own attrs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
|
||||
# assertions:
|
||||
self.assertIsNotNone(c)
|
||||
self.assertIn('<headphones.config.Config', c.__str__())
|
||||
|
||||
# ===========================================================
|
||||
# SET ATTR
|
||||
# ===========================================================
|
||||
|
||||
@TestArgs(
|
||||
('ADD_ALBUM_ART', True),
|
||||
('ALBUM_ART_FORMAT', 'shmolder'),
|
||||
('API_ENABLED', 1),
|
||||
('API_KEY', 'Hello'),
|
||||
)
|
||||
def test__setattr__ConfValuesDefault(self, name, value):
|
||||
""" Config: __setattr__ with setting value explicit """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
act = c.__setattr__(name, value)
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(self.config_mock["General"][name.lower()], value)
|
||||
self.assertEqual(act, value)
|
||||
|
||||
def test__setattr__ExplicitSetUsingDotNotation(self):
|
||||
""" Config: __setattr__ with setting values using dot notation """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
act1 = c.ALBUM_ART_FORMAT = 'Apple'
|
||||
act2 = c.API_ENABLED = True
|
||||
act3 = c.API_KEY = 123
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(self.config_mock["General"]['album_art_format'], 'Apple')
|
||||
self.assertEqual(self.config_mock["General"]['api_enabled'], 1)
|
||||
self.assertEqual(self.config_mock["General"]['api_key'], '123')
|
||||
|
||||
self.assertEqual(act1, 'Apple')
|
||||
self.assertEqual(act2, 1)
|
||||
|
||||
# TODO : check this trange behaviour. I have expected to see here '123', not 123.
|
||||
self.assertEqual(act3, 123)
|
||||
|
||||
# ===========================================================
|
||||
# NEWZNABS
|
||||
#
|
||||
|
||||
@TestArgs(
|
||||
('', []),
|
||||
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
||||
(['ABC', 'DEF'], []),
|
||||
([1], []),
|
||||
([1, 2], []),
|
||||
([1, 2, 3], [(1, 2, 3)]),
|
||||
|
||||
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
)
|
||||
def test_get_extra_newznabs(self, conf_value, expected):
|
||||
""" Config: get_extra_newznabs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
||||
# set up mocks:
|
||||
# 'EXTRA_NEWZNABS': (list, 'Newznab', ''),
|
||||
# 'EXTRA_TORZNABS': (list, 'Torznab', ''),
|
||||
self.config_mock["Newznab"] = {"extra_newznabs": conf_value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.get_extra_newznabs()
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(res, expected)
|
||||
|
||||
def test_clear_extra_newznabs(self):
|
||||
""" Config: clear_extra_newznabs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
random_value = 1827746
|
||||
self.config_mock["Newznab"] = {"extra_newznabs": [1, 2, 3]}
|
||||
self.config_mock["Newznab"] = {"do_not_touch": random_value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.clear_extra_newznabs()
|
||||
|
||||
# assertions:
|
||||
self.assertIsNone(res)
|
||||
self.assertEqual(self.config_mock["Newznab"]["extra_newznabs"], [])
|
||||
self.assertEqual(self.config_mock["Newznab"]["do_not_touch"], random_value)
|
||||
|
||||
@TestArgs(
|
||||
([], [''], ['']),
|
||||
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
||||
|
||||
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
||||
)
|
||||
def test_add_extra_newznab(self, initial, added, expected):
|
||||
""" Config: add_extra_newznab """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["Newznab"] = {"extra_newznabs": initial}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
c.add_extra_newznab(added)
|
||||
act = self.config_mock["Newznab"]["extra_newznabs"]
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(act, expected)
|
||||
|
||||
@TestArgs(
|
||||
(None),
|
||||
([]),
|
||||
([1, 2, 3]),
|
||||
([True]),
|
||||
)
|
||||
def test_add_extra_newznab_raise_on_none(self, initial):
|
||||
""" Config: add_extra_newznab should raise on None adding"""
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["Newznab"] = {"extra_newznabs": initial}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
with self.assertRaises(TypeError):
|
||||
c.add_extra_newznab(None)
|
||||
pass
|
||||
|
||||
# ===========================================================
|
||||
# TORZNABS
|
||||
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
|
||||
#
|
||||
@TestArgs(
|
||||
('', []),
|
||||
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
||||
(['ABC', 'DEF'], []),
|
||||
([1], []),
|
||||
([1, 2], []),
|
||||
([1, 2, 3], [(1, 2, 3)]),
|
||||
|
||||
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||
)
|
||||
def test_get_extra_torznabs(self, conf_value, expected):
|
||||
""" Config: get_extra_torznabs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
# itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
||||
# set up mocks:
|
||||
# 'EXTRA_TORZNABS': (list, '', ''),
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.get_extra_torznabs()
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(res, expected)
|
||||
|
||||
def test_clear_extra_torznabs(self):
|
||||
""" Config: clear_extra_torznabs """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
random_value = -1292721
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
||||
self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
res = c.clear_extra_torznabs()
|
||||
|
||||
# assertions:
|
||||
self.assertIsNone(res)
|
||||
self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
||||
self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
||||
|
||||
@TestArgs(
|
||||
([], [''], ['']),
|
||||
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
||||
|
||||
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
||||
)
|
||||
def test_add_extra_torznab(self, initial, added, expected):
|
||||
""" Config: add_extra_torznab """
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
c.add_extra_torznab(added)
|
||||
act = self.config_mock["Torznab"]["extra_torznabs"]
|
||||
|
||||
# assertions:
|
||||
self.assertEqual(act, expected)
|
||||
|
||||
@TestArgs(
|
||||
(None),
|
||||
([]),
|
||||
([1, 2, 3]),
|
||||
([True]),
|
||||
)
|
||||
def test_add_extra_torznab_raise_on_none(self, initial):
|
||||
""" Config: add_extra_torznab should raise on None adding"""
|
||||
path = '/tmp/notexist'
|
||||
|
||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||
|
||||
# call methods
|
||||
c = headphones.config.Config(path)
|
||||
with self.assertRaises(TypeError):
|
||||
c.add_extra_torznab(None)
|
||||
pass
|
||||
@@ -0,0 +1,36 @@
|
||||
import pprint
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def cry():
|
||||
"""
|
||||
Logs thread traces.
|
||||
"""
|
||||
tmap = {}
|
||||
main_thread = None
|
||||
# get a map of threads by their ID so we can print their names
|
||||
# during the traceback dump
|
||||
for t in threading.enumerate():
|
||||
if t.ident:
|
||||
tmap[t.ident] = t
|
||||
else:
|
||||
main_thread = t
|
||||
|
||||
# Loop over each thread's current frame, writing info about it
|
||||
for tid, frame in sys._current_frames().iteritems():
|
||||
thread = tmap.get(tid, main_thread)
|
||||
|
||||
lines = []
|
||||
lines.append('%s\n' % thread.getName())
|
||||
lines.append('========================================\n')
|
||||
lines += traceback.format_stack(frame)
|
||||
lines.append('========================================\n')
|
||||
lines.append('LOCAL VARIABLES:\n')
|
||||
lines.append('========================================\n')
|
||||
lines.append(pprint.pformat(frame.f_locals))
|
||||
lines.append('\n\n')
|
||||
logger.info("".join(lines))
|
||||
+34
-29
@@ -15,13 +15,13 @@
|
||||
|
||||
# Most of this lifted from here: https://github.com/SzieberthAdam/gneposis-cdgrab
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import copy
|
||||
import glob
|
||||
|
||||
import os
|
||||
import re
|
||||
import headphones
|
||||
from headphones import logger
|
||||
from mutagen.flac import FLAC
|
||||
@@ -62,9 +62,7 @@ WAVE_FILE_TYPE_BY_EXTENSION = {
|
||||
'.flac': 'Free Lossless Audio Codec'
|
||||
}
|
||||
|
||||
# TODO: Only alow flac for now
|
||||
#SHNTOOL_COMPATIBLE = ('Waveform Audio', 'WavPack', 'Free Lossless Audio Codec')
|
||||
SHNTOOL_COMPATIBLE = ('Free Lossless Audio Codec')
|
||||
# SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio")
|
||||
|
||||
# TODO: Make this better!
|
||||
# this module-level variable is bad. :(
|
||||
@@ -109,10 +107,10 @@ def split_baby(split_file, split_cmd):
|
||||
env['PATH'] += os.pathsep + headphones.CONFIG.CUE_SPLIT_FLAC_PATH
|
||||
|
||||
process = subprocess.Popen(split_cmd, startupinfo=startupinfo,
|
||||
|
||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, env=env)
|
||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, env=env)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode:
|
||||
logger.error('Split failed for %s', split_file.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
out = stdout if stdout else stderr
|
||||
@@ -290,7 +288,7 @@ class CueFile(File):
|
||||
global line_content
|
||||
c = self.content.splitlines()
|
||||
header_dict = {}
|
||||
#remaining_headers = CUE_HEADER
|
||||
# remaining_headers = CUE_HEADER
|
||||
remaining_headers = copy.copy(CUE_HEADER)
|
||||
line_index = 0
|
||||
match = True
|
||||
@@ -316,7 +314,8 @@ class CueFile(File):
|
||||
line_content = c[line_index]
|
||||
search_result = re.search(CUE_TRACK, line_content, re.I)
|
||||
if not search_result:
|
||||
raise ValueError('inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1))
|
||||
raise ValueError(
|
||||
'inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1))
|
||||
track_nr = int(search_result.group(1))
|
||||
line_index += 1
|
||||
next_track = False
|
||||
@@ -355,7 +354,8 @@ class CueFile(File):
|
||||
track_meta['dcpflag'] = True
|
||||
line_index += 1
|
||||
else:
|
||||
raise ValueError('unknown entry in track error, line {0}'.format(line_index + 1))
|
||||
raise ValueError(
|
||||
'unknown entry in track error, line {0}'.format(line_index + 1))
|
||||
else:
|
||||
next_track = True
|
||||
|
||||
@@ -373,8 +373,8 @@ class CueFile(File):
|
||||
|
||||
if not self.content:
|
||||
try:
|
||||
with open(self.name, encoding="cp1252") as cue_file:
|
||||
self.content = cue_file.read()
|
||||
with open(self.name, encoding="cp1252") as cue_file:
|
||||
self.content = cue_file.read()
|
||||
except:
|
||||
raise ValueError('Cant encode CUE Sheet.')
|
||||
|
||||
@@ -408,9 +408,11 @@ class CueFile(File):
|
||||
for i in range(len(self.tracks)):
|
||||
if self.tracks[i]:
|
||||
if self.tracks[i].get('artist'):
|
||||
content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get('artist') + '\n'
|
||||
content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get(
|
||||
'artist') + '\n'
|
||||
if self.tracks[i].get('title'):
|
||||
content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get('title') + '\n'
|
||||
content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get(
|
||||
'title') + '\n'
|
||||
return content
|
||||
|
||||
def htoa(self):
|
||||
@@ -451,7 +453,8 @@ class MetaFile(File):
|
||||
raise ValueError('Syntax error in album meta file')
|
||||
if not content['tracks'][int(parsed_track.group(1))]:
|
||||
content['tracks'][int(parsed_track.group(1))] = dict()
|
||||
content['tracks'][int(parsed_track.group(1))][parsed_track.group(2)] = parsed_line.group(2)
|
||||
content['tracks'][int(parsed_track.group(1))][
|
||||
parsed_track.group(2)] = parsed_line.group(2)
|
||||
else:
|
||||
content[parsed_line.group(1)] = parsed_line.group(2)
|
||||
|
||||
@@ -474,15 +477,16 @@ class MetaFile(File):
|
||||
if 'genre' in CUE_META.content:
|
||||
common_tags['genre'] = CUE_META.content['genre']
|
||||
|
||||
#freeform tags
|
||||
#freeform_tags['country'] = self.content['country']
|
||||
#freeform_tags['releasedate'] = self.content['releasedate']
|
||||
# freeform tags
|
||||
# freeform_tags['country'] = self.content['country']
|
||||
# freeform_tags['releasedate'] = self.content['releasedate']
|
||||
|
||||
return common_tags, freeform_tags
|
||||
|
||||
def folders(self):
|
||||
artist = self.content['artist']
|
||||
album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content['label'] + ' - ' + self.content['catalog'] + ')'
|
||||
album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content[
|
||||
'label'] + ' - ' + self.content['catalog'] + ')'
|
||||
return artist, album
|
||||
|
||||
def complete(self):
|
||||
@@ -537,6 +541,7 @@ class WaveFile(File):
|
||||
if self.type == 'Free Lossless Audio Codec':
|
||||
return FLAC(self.name)
|
||||
|
||||
|
||||
def split(albumpath):
|
||||
global CUE_META
|
||||
os.chdir(albumpath)
|
||||
@@ -579,7 +584,8 @@ def split(albumpath):
|
||||
import getXldProfile
|
||||
xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
|
||||
if not xldformat:
|
||||
raise ValueError('Details for xld profile "%s" not found, cannot split cue' % (xldprofile))
|
||||
raise ValueError(
|
||||
'Details for xld profile "%s" not found, cannot split cue' % (xldprofile))
|
||||
else:
|
||||
if headphones.CONFIG.ENCODERFOLDER:
|
||||
splitter = os.path.join(headphones.CONFIG.ENCODERFOLDER, 'xld')
|
||||
@@ -592,11 +598,10 @@ def split(albumpath):
|
||||
splitter = 'shntool'
|
||||
|
||||
if splitter == 'shntool' and not check_splitter(splitter):
|
||||
raise ValueError('Command not found, ensure shntool with FLAC or xld (OS X) installed')
|
||||
raise ValueError('Command not found, ensure shntool or xld installed')
|
||||
|
||||
# Determine if file can be split (only flac allowed for shntool)
|
||||
if 'xld' in splitter and wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys() or \
|
||||
wave.type not in SHNTOOL_COMPATIBLE:
|
||||
# Determine if file can be split
|
||||
if wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys():
|
||||
raise ValueError('Cannot split, audio file has unsupported extension')
|
||||
|
||||
# Split with xld
|
||||
@@ -640,7 +645,7 @@ def split(albumpath):
|
||||
cmd.extend(['-f'])
|
||||
cmd.extend([SPLIT_FILE_NAME])
|
||||
cmd.extend(['-o'])
|
||||
cmd.extend(['flac'])
|
||||
cmd.extend([wave.name_ext.lstrip('.')])
|
||||
cmd.extend([wave.name])
|
||||
split = split_baby(wave.name, cmd)
|
||||
os.remove(SPLIT_FILE_NAME)
|
||||
@@ -652,9 +657,9 @@ def split(albumpath):
|
||||
logger.info('Tagging %s...', t.name)
|
||||
t.tag()
|
||||
|
||||
# rename FLAC files
|
||||
if split and CUE_META.count_tracks() == len(base_dir.tracks(ext='.flac', split=True)):
|
||||
for t in base_dir.tracks(ext='.flac', split=True):
|
||||
# rename files
|
||||
if split and CUE_META.count_tracks() == len(base_dir.tracks(ext=wave.name_ext, split=True)):
|
||||
for t in base_dir.tracks(ext=wave.name_ext, split=True):
|
||||
if t.name != t.filename():
|
||||
logger.info('Renaming %s to %s...', t.name, t.filename())
|
||||
os.rename(t.name, t.filename())
|
||||
|
||||
+16
-16
@@ -13,44 +13,41 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#####################################
|
||||
## Stolen from Sick-Beard's db.py ##
|
||||
#####################################
|
||||
###################################
|
||||
# Stolen from Sick-Beard's db.py #
|
||||
###################################
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
import os
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def dbFilename(filename="headphones.db"):
|
||||
|
||||
return os.path.join(headphones.DATA_DIR, filename)
|
||||
|
||||
|
||||
def getCacheSize():
|
||||
#this will protect against typecasting problems produced by empty string and None settings
|
||||
# this will protect against typecasting problems produced by empty string and None settings
|
||||
if not headphones.CONFIG.CACHE_SIZEMB:
|
||||
#sqlite will work with this (very slowly)
|
||||
# sqlite will work with this (very slowly)
|
||||
return 0
|
||||
return int(headphones.CONFIG.CACHE_SIZEMB)
|
||||
|
||||
|
||||
class DBConnection:
|
||||
|
||||
def __init__(self, filename="headphones.db"):
|
||||
|
||||
self.filename = filename
|
||||
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
||||
#don't wait for the disk to finish writing
|
||||
# don't wait for the disk to finish writing
|
||||
self.connection.execute("PRAGMA synchronous = OFF")
|
||||
#journal disabled since we never do rollbacks
|
||||
# journal disabled since we never do rollbacks
|
||||
self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE)
|
||||
#64mb of cache memory,probably need to make it user configurable
|
||||
# 64mb of cache memory,probably need to make it user configurable
|
||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
|
||||
@@ -92,17 +89,20 @@ class DBConnection:
|
||||
|
||||
def upsert(self, tableName, valueDict, keyDict):
|
||||
|
||||
def genParams(myDict):
|
||||
return [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
changesBefore = self.connection.total_changes
|
||||
|
||||
genParams = lambda myDict: [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(
|
||||
genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
|
||||
self.action(update_query, valueDict.values() + keyDict.values())
|
||||
|
||||
if self.connection.total_changes == changesBefore:
|
||||
insert_query = (
|
||||
"INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" +
|
||||
"INSERT INTO " + tableName + " (" + ", ".join(
|
||||
valueDict.keys() + keyDict.keys()) + ")" +
|
||||
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
# 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/>.
|
||||
|
||||
# Parts of this file are a part of SickRage.
|
||||
# Author: Mr_Orange <mr_orange@hotmail.it>
|
||||
# URL: http://code.google.com/p/sickbeard/
|
||||
# Adapted for Headphones by <noamgit@gmail.com>
|
||||
# URL: https://github.com/noam09
|
||||
#
|
||||
# SickRage 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.
|
||||
#
|
||||
# SickRage 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 SickRage. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from headphones import logger
|
||||
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import headphones
|
||||
import requests
|
||||
from base64 import b64encode
|
||||
import traceback
|
||||
|
||||
delugeweb_auth = {}
|
||||
delugeweb_url = ''
|
||||
deluge_verify_cert = False
|
||||
scrub_logs = True
|
||||
|
||||
|
||||
def _scrubber(text):
|
||||
if scrub_logs:
|
||||
try:
|
||||
# URL parameter values
|
||||
text = re.sub('=[0-9a-zA-Z]*', '=REMOVED', text)
|
||||
# Local host with port
|
||||
# text = re.sub('\:\/\/.*\:', '://REMOVED:', text) # just host
|
||||
text = re.sub('\:\/\/.*\:[0-9]*', '://REMOVED:', text)
|
||||
# Session cookie
|
||||
text = re.sub("_session_id'\: '.*'", "_session_id': 'REMOVED'", text)
|
||||
# Local Windows user path
|
||||
if text.lower().startswith('c:\\users\\'):
|
||||
k = text.split('\\')
|
||||
text = '\\'.join([k[0], k[1], '.....', k[-1]])
|
||||
# partial_link = re.sub('(auth.*?)=.*&','\g<1>=SECRETZ&', link)
|
||||
# partial_link = re.sub('(\w)=[0-9a-zA-Z]*&*','\g<1>=REMOVED&', link)
|
||||
except Exception as e:
|
||||
logger.debug('Deluge: Scrubber failed: %s' % str(e))
|
||||
return text
|
||||
|
||||
|
||||
def addTorrent(link, data=None, name=None):
|
||||
try:
|
||||
# Authenticate anyway
|
||||
logger.debug('Deluge: addTorrent Authentication')
|
||||
_get_auth()
|
||||
|
||||
result = {}
|
||||
retid = False
|
||||
url_what = ['https://what.cd/', 'http://what.cd/']
|
||||
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
||||
|
||||
if link.lower().startswith('magnet:'):
|
||||
logger.debug('Deluge: Got a magnet link: %s' % _scrubber(link))
|
||||
result = {'type': 'magnet',
|
||||
'url': link}
|
||||
retid = _add_torrent_magnet(result)
|
||||
|
||||
elif link.lower().startswith('http://') or link.lower().startswith('https://'):
|
||||
logger.debug('Deluge: Got a URL: %s' % _scrubber(link))
|
||||
if link.lower().startswith(tuple(url_waffles)):
|
||||
if 'rss=' not in link:
|
||||
link = link + '&rss=1'
|
||||
if link.lower().startswith(tuple(url_what)):
|
||||
logger.debug('Deluge: Using different User-Agent for this site')
|
||||
user_agent = 'Headphones'
|
||||
# This method will make Deluge download the file
|
||||
# logger.debug('Deluge: Letting Deluge download this')
|
||||
# local_torrent_path = _add_torrent_url({'url': link})
|
||||
# logger.debug('Deluge: Returned this local path: %s' % _scrubber(local_torrent_path))
|
||||
# return addTorrent(local_torrent_path)
|
||||
else:
|
||||
user_agent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||
headers = {'User-Agent': user_agent}
|
||||
torrentfile = ''
|
||||
logger.debug('Deluge: Trying to download (GET)')
|
||||
try:
|
||||
r = requests.get(link, headers=headers)
|
||||
if r.status_code == 200:
|
||||
logger.debug('Deluge: 200 OK')
|
||||
# .text will ruin the encoding for some torrents
|
||||
torrentfile = r.content
|
||||
else:
|
||||
logger.debug('Deluge: Trying to GET %s returned status %d' % (_scrubber(link), r.status_code))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug('Deluge: Download failed: %s' % str(e))
|
||||
if 'announce' not in str(torrentfile)[:40]:
|
||||
logger.debug('Deluge: Contents of %s doesn\'t look like a torrent file' % _scrubber(link))
|
||||
return False
|
||||
if not name:
|
||||
# Extract torrent name from .torrent
|
||||
try:
|
||||
logger.debug('Deluge: Getting torrent name length')
|
||||
name_length = int(re.findall('name([0-9]*)\:.*?\:', str(torrentfile))[0])
|
||||
logger.debug('Deluge: Getting torrent name')
|
||||
name = re.findall('name[0-9]*\:(.*?)\:', str(torrentfile))[0][:name_length]
|
||||
except Exception as e:
|
||||
logger.debug('Deluge: Could not get torrent name, getting file name')
|
||||
# get last part of link/path (name only)
|
||||
name = link.split('\\')[-1].split('/')[-1]
|
||||
# remove '.torrent' suffix
|
||||
if name[-len('.torrent'):] == '.torrent':
|
||||
name = name[:-len('.torrent')]
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
result = {'type': 'torrent',
|
||||
'name': name,
|
||||
'content': torrentfile}
|
||||
retid = _add_torrent_file(result)
|
||||
|
||||
# elif link.endswith('.torrent') or data:
|
||||
elif not (link.lower().startswith('http://') or link.lower().startswith('https://')):
|
||||
if data:
|
||||
logger.debug('Deluge: Getting .torrent data')
|
||||
torrentfile = data
|
||||
else:
|
||||
logger.debug('Deluge: Getting .torrent file')
|
||||
with open(link, 'rb') as f:
|
||||
torrentfile = f.read()
|
||||
if not name:
|
||||
# Extract torrent name from .torrent
|
||||
try:
|
||||
logger.debug('Deluge: Getting torrent name length')
|
||||
name_length = int(re.findall('name([0-9]*)\:.*?\:', str(torrentfile))[0])
|
||||
logger.debug('Deluge: Getting torrent name')
|
||||
name = re.findall('name[0-9]*\:(.*?)\:', str(torrentfile))[0][:name_length]
|
||||
except Exception as e:
|
||||
logger.debug('Deluge: Could not get torrent name, getting file name')
|
||||
# get last part of link/path (name only)
|
||||
name = link.split('\\')[-1].split('/')[-1]
|
||||
# remove '.torrent' suffix
|
||||
if name[-len('.torrent'):] == '.torrent':
|
||||
name = name[:-len('.torrent')]
|
||||
logger.debug('Deluge: Sending Deluge torrent with name %s and content [%s...]' % (name, str(torrentfile)[:40]))
|
||||
result = {'type': 'torrent',
|
||||
'name': name,
|
||||
'content': torrentfile}
|
||||
retid = _add_torrent_file(result)
|
||||
|
||||
else:
|
||||
logger.error('Deluge: Unknown file type: %s' % link)
|
||||
|
||||
if retid:
|
||||
logger.info('Deluge: Torrent sent to Deluge successfully (%s)' % retid)
|
||||
return retid
|
||||
else:
|
||||
logger.info('Deluge: Returned status %s' % retid)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
|
||||
|
||||
def getTorrentFolder(result):
|
||||
logger.debug('Deluge: Get torrent folder name')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
|
||||
try:
|
||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||
"params": [
|
||||
result['hash'],
|
||||
["total_done"]
|
||||
],
|
||||
"id": 21})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||
|
||||
tries = 0
|
||||
while result['total_done'] == 0 and tries < 10:
|
||||
tries += 1
|
||||
time.sleep(5)
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result['total_done'] = json.loads(response.text)['result']['total_done']
|
||||
|
||||
post_data = json.dumps({"method": "web.get_torrent_status",
|
||||
"params": [
|
||||
result['hash'],
|
||||
[
|
||||
"name",
|
||||
"save_path",
|
||||
"total_size",
|
||||
"num_files",
|
||||
"message",
|
||||
"tracker",
|
||||
"comment"
|
||||
]
|
||||
],
|
||||
"id": 23})
|
||||
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
|
||||
result['save_path'] = json.loads(response.text)['result']['save_path']
|
||||
result['name'] = json.loads(response.text)['result']['name']
|
||||
|
||||
return json.loads(response.text)['result']['name']
|
||||
except Exception as e:
|
||||
logger.debug('Deluge: Could not get torrent folder name: %s' % str(e))
|
||||
|
||||
|
||||
def removeTorrent(torrentid, remove_data=False):
|
||||
logger.debug('Deluge: Remove torrent %s' % torrentid)
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
|
||||
try:
|
||||
result = False
|
||||
post_data = json.dumps({"method": "core.remove_torrent",
|
||||
"params": [
|
||||
torrentid,
|
||||
remove_data
|
||||
],
|
||||
"id": 25})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result = json.loads(response.text)['result']
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Removing torrent failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
|
||||
def _get_auth():
|
||||
logger.debug('Deluge: Authenticating...')
|
||||
global delugeweb_auth, delugeweb_url, deluge_verify_cert
|
||||
delugeweb_auth = {}
|
||||
|
||||
delugeweb_host = headphones.CONFIG.DELUGE_HOST
|
||||
delugeweb_cert = headphones.CONFIG.DELUGE_CERT
|
||||
delugeweb_password = headphones.CONFIG.DELUGE_PASSWORD
|
||||
if len(delugeweb_password) > 0:
|
||||
logger.debug('Deluge: Using password %s******%s' % (delugeweb_password[0], delugeweb_password[-1]))
|
||||
|
||||
if not delugeweb_host.startswith('http'):
|
||||
delugeweb_host = 'http://%s' % delugeweb_host
|
||||
|
||||
if delugeweb_cert is None or delugeweb_cert.strip() == '':
|
||||
deluge_verify_cert = False
|
||||
logger.debug('Deluge: FYI no SSL certificate configured')
|
||||
else:
|
||||
deluge_verify_cert = delugeweb_cert
|
||||
delugeweb_host = delugeweb_host.replace('http:', 'https:')
|
||||
logger.debug('Deluge: Using certificate %s, host is now %s' % (_scrubber(deluge_verify_cert), _scrubber(delugeweb_host)))
|
||||
|
||||
if delugeweb_host.endswith('/'):
|
||||
delugeweb_host = delugeweb_host[:-1]
|
||||
|
||||
delugeweb_url = delugeweb_host + '/json'
|
||||
|
||||
post_data = json.dumps({"method": "auth.login",
|
||||
"params": [delugeweb_password],
|
||||
"id": 1})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
except requests.ConnectionError:
|
||||
try:
|
||||
logger.debug('Deluge: Connection failed, let\'s try HTTPS just in case')
|
||||
response = requests.post(delugeweb_url.replace('http:', 'https:'), data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
# If the previous line didn't fail, change delugeweb_url for the rest of this session
|
||||
logger.error('Deluge: Switching to HTTPS, but certificate won\'t be verified because NO CERTIFICATE WAS CONFIGURED!')
|
||||
delugeweb_url = delugeweb_url.replace('http:', 'https:')
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
auth = json.loads(response.text)["result"]
|
||||
auth_error = json.loads(response.text)["error"]
|
||||
logger.debug('Deluge: Authentication result: %s, Error: %s' % (auth, auth_error))
|
||||
delugeweb_auth = response.cookies
|
||||
logger.debug('Deluge: Authentication cookies: %s' % _scrubber(str(delugeweb_auth.get_dict())))
|
||||
post_data = json.dumps({"method": "web.connected",
|
||||
"params": [],
|
||||
"id": 10})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
connected = json.loads(response.text)['result']
|
||||
connected_error = json.loads(response.text)['error']
|
||||
logger.debug('Deluge: Connection result: %s, Error: %s' % (connected, connected_error))
|
||||
|
||||
if not connected:
|
||||
post_data = json.dumps({"method": "web.get_hosts",
|
||||
"params": [],
|
||||
"id": 11})
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
delugeweb_hosts = json.loads(response.text)['result']
|
||||
if len(delugeweb_hosts) == 0:
|
||||
logger.error('Deluge: WebUI does not contain daemons')
|
||||
return None
|
||||
|
||||
post_data = json.dumps({"method": "web.connect",
|
||||
"params": [delugeweb_hosts[0][0]],
|
||||
"id": 11})
|
||||
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
post_data = json.dumps({"method": "web.connected",
|
||||
"params": [],
|
||||
"id": 10})
|
||||
|
||||
try:
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Authentication failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
connected = json.loads(response.text)['result']
|
||||
|
||||
if not connected:
|
||||
logger.error('Deluge: WebUI could not connect to daemon')
|
||||
return None
|
||||
|
||||
return auth
|
||||
|
||||
|
||||
def _add_torrent_magnet(result):
|
||||
logger.debug('Deluge: Adding magnet')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
try:
|
||||
post_data = json.dumps({"method": "core.add_torrent_magnet",
|
||||
"params": [result['url'], {}],
|
||||
"id": 2})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Adding torrent magnet failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return False
|
||||
|
||||
|
||||
def _add_torrent_url(result):
|
||||
logger.debug('Deluge: Adding URL')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
try:
|
||||
post_data = json.dumps({"method": "web.download_torrent_from_url",
|
||||
"params": [result['url'], {}],
|
||||
"id": 32})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result['location'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Adding torrent URL failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return False
|
||||
|
||||
|
||||
def _add_torrent_file(result):
|
||||
logger.debug('Deluge: Adding file')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
try:
|
||||
# content is torrent file contents that needs to be encoded to base64
|
||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||
"params": [result['name'] + '.torrent', b64encode(result['content'].encode('utf8')), {}],
|
||||
"id": 2})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
# content is torrent file contents that needs to be encoded to base64
|
||||
# this time let's try leaving the encoding as is
|
||||
post_data = json.dumps({"method": "core.add_torrent_file",
|
||||
"params": [result['name'] + '.torrent', b64encode(result['content']), {}],
|
||||
"id": 22})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
result['hash'] = json.loads(response.text)['result']
|
||||
logger.debug('Deluge: Response was %s' % str(json.loads(response.text)))
|
||||
return json.loads(response.text)['result']
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Adding torrent file failed after decode: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Adding torrent file failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return False
|
||||
|
||||
|
||||
def setTorrentLabel(result):
|
||||
logger.debug('Deluge: Setting label')
|
||||
label = headphones.CONFIG.DELUGE_LABEL
|
||||
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
|
||||
if ' ' in label:
|
||||
logger.error('Deluge: Invalid label. Label can\'t contain spaces - replacing with underscores')
|
||||
label = label.replace(' ', '_')
|
||||
if label:
|
||||
# check if label already exists and create it if not
|
||||
post_data = json.dumps({"method": 'label.get_labels',
|
||||
"params": [],
|
||||
"id": 3})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
labels = json.loads(response.text)['result']
|
||||
|
||||
if labels is not None:
|
||||
if label not in labels:
|
||||
try:
|
||||
logger.debug('Deluge: %s label doesn\'t exist in Deluge, let\'s add it' % label)
|
||||
post_data = json.dumps({"method": 'label.add',
|
||||
"params": [label],
|
||||
"id": 4})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
logger.debug('Deluge: %s label added to Deluge' % label)
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Setting label failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
|
||||
# add label to torrent
|
||||
post_data = json.dumps({"method": 'label.set_torrent',
|
||||
"params": [result['hash'], label],
|
||||
"id": 5})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
logger.debug('Deluge: %s label added to torrent' % label)
|
||||
else:
|
||||
logger.debug('Deluge: Label plugin not detected')
|
||||
return False
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
|
||||
def setSeedRatio(result):
|
||||
logger.debug('Deluge: Setting seed ratio')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
|
||||
ratio = None
|
||||
if result['ratio']:
|
||||
ratio = result['ratio']
|
||||
|
||||
try:
|
||||
if ratio:
|
||||
post_data = json.dumps({"method": "core.set_torrent_stop_at_ratio",
|
||||
"params": [result['hash'], True],
|
||||
"id": 5})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
post_data = json.dumps({"method": "core.set_torrent_stop_ratio",
|
||||
"params": [result['hash'], float(ratio)],
|
||||
"id": 6})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Setting seed ratio failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
|
||||
def setTorrentPath(result):
|
||||
logger.debug('Deluge: Setting download path')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
|
||||
try:
|
||||
if headphones.CONFIG.DELUGE_DONE_DIRECTORY or headphones.CONFIG.DOWNLOAD_TORRENT_DIR:
|
||||
post_data = json.dumps({"method": "core.set_torrent_move_completed",
|
||||
"params": [result['hash'], True],
|
||||
"id": 7})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
|
||||
if headphones.CONFIG.DELUGE_DONE_DIRECTORY:
|
||||
move_to = headphones.CONFIG.DELUGE_DONE_DIRECTORY
|
||||
else:
|
||||
move_to = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
|
||||
|
||||
if not os.path.exists(move_to):
|
||||
logger.debug('Deluge: %s directory doesn\'t exist, let\'s create it' % move_to)
|
||||
os.makedirs(move_to)
|
||||
post_data = json.dumps({"method": "core.set_torrent_move_completed_path",
|
||||
"params": [result['hash'], move_to],
|
||||
"id": 8})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Setting torrent move-to directory failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
|
||||
|
||||
def setTorrentPause(result):
|
||||
logger.debug('Deluge: Pausing torrent')
|
||||
if not any(delugeweb_auth):
|
||||
_get_auth()
|
||||
|
||||
try:
|
||||
if headphones.CONFIG.DELUGE_PAUSED:
|
||||
post_data = json.dumps({"method": "core.pause_torrent",
|
||||
"params": [[result['hash']]],
|
||||
"id": 9})
|
||||
response = requests.post(delugeweb_url, data=post_data.encode('utf-8'), cookies=delugeweb_auth,
|
||||
verify=deluge_verify_cert)
|
||||
|
||||
return not json.loads(response.text)['error']
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error('Deluge: Setting torrent paused failed: %s' % str(e))
|
||||
formatted_lines = traceback.format_exc().splitlines()
|
||||
logger.error('; '.join(formatted_lines))
|
||||
return None
|
||||
@@ -24,3 +24,10 @@ class NewzbinAPIThrottled(HeadphonesException):
|
||||
"""
|
||||
Newzbin has throttled us, deal with it
|
||||
"""
|
||||
|
||||
|
||||
class SoftChrootError(HeadphonesException):
|
||||
"""
|
||||
Fatal errors in SoftChroot module
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import os.path
|
||||
|
||||
import biplist
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def getXldProfile(xldProfile):
|
||||
|
||||
xldProfileNotFound = xldProfile
|
||||
|
||||
expanded = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist')
|
||||
if not os.path.isfile(expanded):
|
||||
logger.warn("Could not find xld preferences at: %s", expanded)
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
# Get xld preferences plist
|
||||
try:
|
||||
preferences = biplist.readPlist(expanded)
|
||||
except (biplist.InvalidPlistException, biplist.NotBinaryPlistException), e:
|
||||
logger.error("Error reading xld preferences plist: %s", e)
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
if not isinstance(preferences, dict):
|
||||
logger.error("Error reading xld preferences plist, not a dict: %r", preferences)
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
profiles = preferences.get('Profiles', []) # pylint:disable=E1103
|
||||
profiles = preferences.get('Profiles', []) # pylint:disable=E1103
|
||||
|
||||
xldProfile = xldProfile.lower()
|
||||
for profile in profiles:
|
||||
@@ -175,6 +175,6 @@ def getXldProfile(xldProfile):
|
||||
if xldFormat and not xldBitrate:
|
||||
xldBitrate = 400
|
||||
|
||||
return(xldProfileForCmd, xldFormat, xldBitrate)
|
||||
return (xldProfileForCmd, xldFormat, xldBitrate)
|
||||
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
+176
-63
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones is free software: you can redistribute it and/or modify
|
||||
@@ -13,19 +14,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
import unicodedata
|
||||
import headphones
|
||||
import datetime
|
||||
import fnmatch
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
import os
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
import headphones
|
||||
|
||||
|
||||
# Modified from https://github.com/Verrus/beets-plugin-featInTitle
|
||||
RE_FEATURING = re.compile(r"[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.")
|
||||
@@ -35,7 +36,9 @@ RE_CD = re.compile(r"^(CD|dics)\s*[0-9]+$", re.I)
|
||||
|
||||
|
||||
def multikeysort(items, columns):
|
||||
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
||||
comparers = [
|
||||
((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1))
|
||||
for col in columns]
|
||||
|
||||
def comparer(left, right):
|
||||
for fn, mult in comparers:
|
||||
@@ -56,7 +59,6 @@ def checked(variable):
|
||||
|
||||
|
||||
def radio(variable, pos):
|
||||
|
||||
if variable == pos:
|
||||
return 'Checked'
|
||||
else:
|
||||
@@ -70,7 +72,7 @@ def latinToAscii(unicrap):
|
||||
xlate = {
|
||||
0xc0: 'A', 0xc1: 'A', 0xc2: 'A', 0xc3: 'A', 0xc4: 'A', 0xc5: 'A',
|
||||
0xc6: 'Ae', 0xc7: 'C',
|
||||
0xc8: 'E', 0xc9: 'E', 0xca: 'E', 0xcb: 'E', 0x86: 'e',
|
||||
0xc8: 'E', 0xc9: 'E', 0xca: 'E', 0xcb: 'E', 0x86: 'e', 0x39e: 'E',
|
||||
0xcc: 'I', 0xcd: 'I', 0xce: 'I', 0xcf: 'I',
|
||||
0xd0: 'Th', 0xd1: 'N',
|
||||
0xd2: 'O', 0xd3: 'O', 0xd4: 'O', 0xd5: 'O', 0xd6: 'O', 0xd8: 'O',
|
||||
@@ -107,7 +109,6 @@ def latinToAscii(unicrap):
|
||||
|
||||
|
||||
def convert_milliseconds(ms):
|
||||
|
||||
seconds = ms / 1000
|
||||
gmtime = time.gmtime(seconds)
|
||||
if seconds > 3600:
|
||||
@@ -119,7 +120,6 @@ def convert_milliseconds(ms):
|
||||
|
||||
|
||||
def convert_seconds(s):
|
||||
|
||||
gmtime = time.gmtime(s)
|
||||
if s > 3600:
|
||||
minutes = time.strftime("%H:%M:%S", gmtime)
|
||||
@@ -141,7 +141,6 @@ def now():
|
||||
|
||||
|
||||
def get_age(date):
|
||||
|
||||
try:
|
||||
split_date = date.split('-')
|
||||
except:
|
||||
@@ -149,14 +148,13 @@ def get_age(date):
|
||||
|
||||
try:
|
||||
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
|
||||
except IndexError:
|
||||
except (IndexError, ValueError):
|
||||
days_old = False
|
||||
|
||||
return days_old
|
||||
|
||||
|
||||
def bytes_to_mb(bytes):
|
||||
|
||||
mb = int(bytes) / 1048576
|
||||
size = '%.1f MB' % mb
|
||||
return size
|
||||
@@ -172,16 +170,16 @@ def piratesize(size):
|
||||
split = size.split(" ")
|
||||
factor = float(split[0])
|
||||
unit = split[1].upper()
|
||||
|
||||
if unit == 'MiB':
|
||||
|
||||
if unit == 'MIB':
|
||||
size = factor * 1048576
|
||||
elif unit == 'MB':
|
||||
size = factor * 1000000
|
||||
elif unit == 'GiB':
|
||||
elif unit == 'GIB':
|
||||
size = factor * 1073741824
|
||||
elif unit == 'GB':
|
||||
size = factor * 1000000000
|
||||
elif unit == 'KiB':
|
||||
elif unit == 'KIB':
|
||||
size = factor * 1024
|
||||
elif unit == 'KB':
|
||||
size = factor * 1000
|
||||
@@ -194,21 +192,25 @@ def piratesize(size):
|
||||
|
||||
|
||||
def replace_all(text, dic, normalize=False):
|
||||
|
||||
from headphones import pathrender
|
||||
if not text:
|
||||
return ''
|
||||
|
||||
for i, j in dic.iteritems():
|
||||
if normalize:
|
||||
try:
|
||||
if sys.platform == 'darwin':
|
||||
j = unicodedata.normalize('NFD', j)
|
||||
else:
|
||||
j = unicodedata.normalize('NFC', j)
|
||||
except TypeError:
|
||||
j = unicodedata.normalize('NFC', j.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
text = text.replace(i, j)
|
||||
return text
|
||||
if normalize:
|
||||
new_dic = {}
|
||||
for i, j in dic.iteritems():
|
||||
if j is not None:
|
||||
try:
|
||||
if sys.platform == 'darwin':
|
||||
j = unicodedata.normalize('NFD', j)
|
||||
else:
|
||||
j = unicodedata.normalize('NFC', j)
|
||||
except TypeError:
|
||||
j = unicodedata.normalize('NFC',
|
||||
j.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
new_dic[i] = j
|
||||
dic = new_dic
|
||||
return pathrender.render(text, dic)[0]
|
||||
|
||||
|
||||
def replace_illegal_chars(string, type="file"):
|
||||
@@ -220,16 +222,108 @@ def replace_illegal_chars(string, type="file"):
|
||||
return string
|
||||
|
||||
|
||||
def cleanName(string):
|
||||
_CN_RE1 = re.compile(ur'[^\w]+', re.UNICODE)
|
||||
_CN_RE2 = re.compile(ur'[\s_]+', re.UNICODE)
|
||||
|
||||
pass1 = latinToAscii(string).lower()
|
||||
out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', pass1).encode('utf-8')
|
||||
|
||||
return out_string
|
||||
_XLATE_GRAPHICAL_AND_DIACRITICAL = {
|
||||
# Translation table.
|
||||
# Covers the following letters, for which NFD fails because of lack of
|
||||
# combining character:
|
||||
# ©ª«®²³¹»¼½¾ÆÐØÞßæðøþĐđĦħıIJijĸĿŀŁłŒœŦŧDŽDždžLJLjljNJNjnjǤǥDZDzdzȤȥ. This
|
||||
# includes also some graphical symbols which can be easily replaced and
|
||||
# usually are written by people who don't have appropriate keyboard layout.
|
||||
u'©': '(C)', u'ª': 'a.', u'«': '<<', u'®': '(R)', u'²': '2', u'³': '3',
|
||||
u'¹': '1', u'»': '>>', u'¼': ' 1/4 ', u'½': ' 1/2 ', u'¾': ' 3/4 ',
|
||||
u'Æ': 'AE', u'Ð': 'D', u'Ø': 'O', u'Þ': 'Th', u'ß': 'ss', u'æ': 'ae',
|
||||
u'ð': 'd', u'ø': 'o', u'þ': 'th', u'Đ': 'D', u'đ': 'd', u'Ħ': 'H',
|
||||
u'ħ': 'h', u'ı': 'i', u'IJ': 'IJ', u'ij': 'ij', u'ĸ': 'q', u'Ŀ': 'L',
|
||||
u'ŀ': 'l', u'Ł': 'L', u'ł': 'l', u'Œ': 'OE', u'œ': 'oe', u'Ŧ': 'T',
|
||||
u'ŧ': 't', u'DŽ': 'DZ', u'Dž': 'Dz', u'LJ': 'LJ', u'Lj': 'Lj',
|
||||
u'lj': 'lj', u'NJ': 'NJ', u'Nj': 'Nj', u'nj': 'nj',
|
||||
u'Ǥ': 'G', u'ǥ': 'g', u'DZ': 'DZ', u'Dz': 'Dz', u'dz': 'dz',
|
||||
u'Ȥ': 'Z', u'ȥ': 'z', u'№': 'No.',
|
||||
u'º': 'o.', # normalize Nº abbrev (popular w/ classical music),
|
||||
# this is 'masculine ordering indicator', not degree
|
||||
}
|
||||
|
||||
_XLATE_SPECIAL = {
|
||||
# Translation table.
|
||||
# Cover additional special characters processing normalization.
|
||||
u"'": '', # replace apostrophe with nothing
|
||||
u"’": '', # replace musicbrainz style apostrophe with nothing
|
||||
u'&': ' and ', # expand & to ' and '
|
||||
}
|
||||
|
||||
|
||||
def _translate(s, dictionary):
|
||||
# type: (basestring,Mapping[basestring,basestring])->basestring
|
||||
return ''.join(dictionary.get(x, x) for x in s)
|
||||
|
||||
|
||||
_COMBINING_RANGES = (
|
||||
(0x0300, 0x036f), # Combining Diacritical Marks
|
||||
(0x1ab0, 0x1aff), # Combining Diacritical Marks Extended
|
||||
(0x20d0, 0x20ff), # Combining Diacritical Marks for Symbols
|
||||
(0x1dc0, 0x1dff) # Combining Diacritical Marks Supplement
|
||||
)
|
||||
|
||||
|
||||
def _is_unicode_combining(u):
|
||||
# type: (unicode)->bool
|
||||
"""
|
||||
Check if input unicode is combining diacritical mark.
|
||||
"""
|
||||
i = ord(u)
|
||||
for r in _COMBINING_RANGES:
|
||||
if r[0] <= i <= r[1]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _transliterate(u, xlate):
|
||||
# type: (unicode)->unicode
|
||||
"""
|
||||
Perform transliteration using the specified dictionary
|
||||
"""
|
||||
u = unicodedata.normalize('NFD', u)
|
||||
u = u''.join([u'' if _is_unicode_combining(x) else x for x in u])
|
||||
u = _translate(u, xlate)
|
||||
# at this point output is either unicode, or plain ascii
|
||||
return unicode(u)
|
||||
|
||||
|
||||
def clean_name(s):
|
||||
# type: (basestring)->unicode
|
||||
"""Remove non-alphanumeric characters from the string, perform
|
||||
normalization and substitution of some special characters; coalesce spaces.
|
||||
:param s: string to clean up, possibly unicode one.
|
||||
:return: cleaned-up version of input string.
|
||||
"""
|
||||
if not isinstance(s, unicode):
|
||||
# ignore extended chars if someone was dumb enough to pass non-ascii
|
||||
# narrow string here, use only unicode for meaningful texts
|
||||
u = unicode(s, 'ascii', 'replace')
|
||||
else:
|
||||
u = s
|
||||
# 1. don't bother doing normalization NFKC, rather transliterate
|
||||
# using special translation table
|
||||
u = _transliterate(u, _XLATE_GRAPHICAL_AND_DIACRITICAL)
|
||||
# 2. normalize NFKC the result
|
||||
u = unicodedata.normalize('NFKC', u)
|
||||
# 3. translate spacials
|
||||
u = _translate(u, _XLATE_SPECIAL)
|
||||
# 4. replace any non-alphanumeric character sequences by spaces
|
||||
u = _CN_RE1.sub(u' ', u)
|
||||
# 5. coalesce interleaved space/underscore sequences
|
||||
u = _CN_RE2.sub(u' ', u)
|
||||
# 6. trim
|
||||
u = u.strip()
|
||||
# 7. lowercase
|
||||
return u
|
||||
|
||||
|
||||
def cleanTitle(title):
|
||||
|
||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||
|
||||
# Strip out extra whitespace
|
||||
@@ -312,16 +406,22 @@ def expand_subfolders(f):
|
||||
difference = max(path_depths) - min(path_depths)
|
||||
|
||||
if difference > 0:
|
||||
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", len(media_folders), difference)
|
||||
logger.info(
|
||||
"Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.",
|
||||
len(media_folders), difference)
|
||||
|
||||
# While already failed, advice the user what he could try. We assume the
|
||||
# directory may contain separate CD's and maybe some extra's. The
|
||||
# structure may look like X albums at same depth, and (one or more)
|
||||
# extra folders with a higher depth.
|
||||
extra_media_folders = [media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths)]
|
||||
extra_media_folders = list(set([os.path.join(*media_folder) for media_folder in extra_media_folders]))
|
||||
extra_media_folders = [media_folder[:min(path_depths)] for media_folder in media_folders if
|
||||
len(media_folder) > min(path_depths)]
|
||||
extra_media_folders = list(
|
||||
set([os.path.join(*media_folder) for media_folder in extra_media_folders]))
|
||||
|
||||
logger.info("Please look at the following folder(s), since they cause the depth difference: %s", extra_media_folders)
|
||||
logger.info(
|
||||
"Please look at the following folder(s), since they cause the depth difference: %s",
|
||||
extra_media_folders)
|
||||
return
|
||||
|
||||
# Convert back to paths and remove duplicates, which may be there after
|
||||
@@ -368,7 +468,7 @@ def path_filter_patterns(paths, patterns, root=None):
|
||||
for path in paths[:]:
|
||||
if path_match_patterns(path, patterns):
|
||||
logger.debug("Path ignored by pattern: %s",
|
||||
os.path.join(root or "", path))
|
||||
os.path.join(root or "", path))
|
||||
|
||||
ignored += 1
|
||||
paths.remove(path)
|
||||
@@ -378,11 +478,11 @@ def path_filter_patterns(paths, patterns, root=None):
|
||||
|
||||
|
||||
def extract_data(s):
|
||||
|
||||
s = s.replace('_', ' ')
|
||||
|
||||
#headphones default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s[\[\(](?P<year>.*?)[\]\)]', re.VERBOSE)
|
||||
# headphones default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s[\[\(](?P<year>.*?)[\]\)]',
|
||||
re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
|
||||
if match:
|
||||
@@ -391,7 +491,7 @@ def extract_data(s):
|
||||
year = match.group("year")
|
||||
return (name, album, year)
|
||||
|
||||
#Gonna take a guess on this one - might be enough to search on mb
|
||||
# Gonna take a guess on this one - might be enough to search on mb
|
||||
pat = re.compile(r"(?P<name>.*?)\s*-\s*(?P<album>[^\[(-]*)")
|
||||
|
||||
match = pat.match(s)
|
||||
@@ -468,7 +568,8 @@ def extract_metadata(f):
|
||||
old_album = new_albums[index]
|
||||
new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
|
||||
|
||||
logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index])
|
||||
logger.debug("Stripped albumd number identifier: %s -> %s", old_album,
|
||||
new_albums[index])
|
||||
|
||||
# Remove duplicates
|
||||
new_albums = list(set(new_albums))
|
||||
@@ -498,7 +599,8 @@ def extract_metadata(f):
|
||||
return (artist, albums[0], years[0])
|
||||
|
||||
# Not sure what to do here.
|
||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years))
|
||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists),
|
||||
len(albums), len(years))
|
||||
logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
|
||||
|
||||
return (None, None, None)
|
||||
@@ -519,19 +621,21 @@ def get_downloaded_track_list(albumpath):
|
||||
return downloaded_track_list
|
||||
|
||||
|
||||
def preserve_torrent_direcory(albumpath):
|
||||
def preserve_torrent_directory(albumpath):
|
||||
"""
|
||||
Copy torrent directory to headphones-modified to keep files for seeding.
|
||||
"""
|
||||
from headphones import logger
|
||||
new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
new_folder = os.path.join(albumpath,
|
||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
try:
|
||||
shutil.copytree(albumpath, new_folder)
|
||||
return new_folder
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp folder: " + \
|
||||
new_folder.decode(headphones.SYS_ENCODING, 'replace') + \
|
||||
logger.warn("Cannot copy/move files to temp folder: " +
|
||||
new_folder.decode(headphones.SYS_ENCODING, 'replace') +
|
||||
". Not continuing. Error: " + str(e))
|
||||
return None
|
||||
|
||||
@@ -578,7 +682,9 @@ def cue_split(albumpath):
|
||||
|
||||
def extract_logline(s):
|
||||
# Default log format
|
||||
pattern = re.compile(r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)', re.VERBOSE)
|
||||
pattern = re.compile(
|
||||
r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)',
|
||||
re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
if match:
|
||||
timestamp = match.group("timestamp")
|
||||
@@ -593,7 +699,7 @@ def extract_logline(s):
|
||||
def extract_song_data(s):
|
||||
from headphones import logger
|
||||
|
||||
#headphones default format
|
||||
# headphones default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\[(?P<year>.*?)\]', re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
|
||||
@@ -605,7 +711,7 @@ def extract_song_data(s):
|
||||
else:
|
||||
logger.info("Couldn't parse %s into a valid default format", s)
|
||||
|
||||
#newzbin default format
|
||||
# newzbin default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\((?P<year>\d+?\))', re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
if match:
|
||||
@@ -619,7 +725,6 @@ def extract_song_data(s):
|
||||
|
||||
|
||||
def smartMove(src, dest, delete=True):
|
||||
|
||||
from headphones import logger
|
||||
|
||||
source_dir = os.path.dirname(src)
|
||||
@@ -640,7 +745,8 @@ def smartMove(src, dest, delete=True):
|
||||
os.rename(src, os.path.join(source_dir, newfile))
|
||||
filename = newfile
|
||||
except Exception as e:
|
||||
logger.warn('Error renaming %s: %s', src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
logger.warn('Error renaming %s: %s',
|
||||
src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
break
|
||||
|
||||
try:
|
||||
@@ -650,7 +756,9 @@ def smartMove(src, dest, delete=True):
|
||||
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
e)
|
||||
|
||||
|
||||
def walk_directory(basedir, followlinks=True):
|
||||
"""
|
||||
@@ -672,8 +780,8 @@ def walk_directory(basedir, followlinks=True):
|
||||
real_path = os.path.abspath(os.readlink(path))
|
||||
|
||||
if real_path in traversed:
|
||||
logger.debug("Skipping '%s' since it is a symlink to "\
|
||||
"'%s', which is already visited.", path, real_path)
|
||||
logger.debug("Skipping '%s' since it is a symlink to "
|
||||
"'%s', which is already visited.", path, real_path)
|
||||
else:
|
||||
traversed.append(real_path)
|
||||
|
||||
@@ -689,8 +797,9 @@ def walk_directory(basedir, followlinks=True):
|
||||
for result in _inner(*args):
|
||||
yield result
|
||||
|
||||
|
||||
#########################
|
||||
#Sab renaming functions #
|
||||
# Sab renaming functions #
|
||||
#########################
|
||||
|
||||
# TODO: Grab config values from sab to know when these options are checked. For now we'll just iterate through all combinations
|
||||
@@ -739,18 +848,20 @@ def sab_sanitize_foldername(name):
|
||||
if not name:
|
||||
name = 'unknown'
|
||||
|
||||
#maxlen = cfg.folder_max_length()
|
||||
#if len(name) > maxlen:
|
||||
# maxlen = cfg.folder_max_length()
|
||||
# if len(name) > maxlen:
|
||||
# name = name[:maxlen]
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def split_string(mystring, splitvar=','):
|
||||
mylist = []
|
||||
for each_word in mystring.split(splitvar):
|
||||
mylist.append(each_word.strip())
|
||||
return mylist
|
||||
|
||||
|
||||
def create_https_certificates(ssl_cert, ssl_key):
|
||||
"""
|
||||
Create a pair of self-signed HTTPS certificares and store in them in
|
||||
@@ -768,11 +879,13 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
# Create the CA Certificate
|
||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
cacert = createCertificate(careq, (careq, cakey), serial,
|
||||
(0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||
req = createCertRequest(pkey, CN="Headphones")
|
||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
cert = createCertificate(req, (cacert, cakey), serial,
|
||||
(0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
# Save the key and certificate to disk
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittestcompat import TestCase
|
||||
from headphones.helpers import clean_name
|
||||
|
||||
|
||||
class HelpersTest(TestCase):
|
||||
|
||||
def test_clean_name(self):
|
||||
"""helpers: check correctness of clean_name() function"""
|
||||
cases = {
|
||||
u' Weiße & rose ': 'Weisse and rose',
|
||||
u'Multiple / spaces': 'Multiple spaces',
|
||||
u'Kevin\'s m²': 'Kevins m2',
|
||||
u'Symphonęy Nº9': 'Symphoney No.9',
|
||||
u'ÆæßðÞIJij': u'AeaessdThIJıj',
|
||||
u'Obsessió (Cerebral Apoplexy remix)': 'obsessio cerebral '
|
||||
'apoplexy remix',
|
||||
u'Doktór Hałabała i siedmiu zbojów': 'doktor halabala i siedmiu '
|
||||
'zbojow',
|
||||
u'Arbetets Söner och Döttrar': 'arbetets soner och dottrar',
|
||||
u'Björk Guðmundsdóttir': 'bjork gudmundsdottir',
|
||||
u'L\'Arc~en~Ciel': 'larc en ciel',
|
||||
u'Orquesta de la Luz (オルケスタ・デ・ラ・ルス)':
|
||||
u'Orquesta de la Luz オルケスタ デ ラ ルス'
|
||||
|
||||
}
|
||||
for first, second in cases.iteritems():
|
||||
nf = clean_name(first).lower()
|
||||
ns = clean_name(second).lower()
|
||||
self.assertEqual(
|
||||
nf, ns, u"check cleaning of case (%s,"
|
||||
u"%s)" % (nf, ns)
|
||||
)
|
||||
|
||||
def test_clean_name_nonunicode(self):
|
||||
"""helpers: check if clean_name() works on non-unicode input"""
|
||||
input = 'foo $ bar/BAZ'
|
||||
test = clean_name(input).lower()
|
||||
expected = 'foo bar baz'
|
||||
self.assertEqual(
|
||||
test, expected, "check clean_name() works on non-unicode"
|
||||
)
|
||||
input = 'fóó $ BAZ'
|
||||
test = clean_name(input).lower()
|
||||
expected = clean_name('%fóó baz ').lower()
|
||||
self.assertEqual(
|
||||
test, expected, "check clean_name() with narrow non-ascii input"
|
||||
)
|
||||
+188
-117
@@ -13,39 +13,39 @@
|
||||
# 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 logger, helpers, db, mb, lastfm
|
||||
|
||||
from beets.mediafile import MediaFile
|
||||
|
||||
import time
|
||||
|
||||
from headphones import logger, helpers, db, mb, lastfm, metacritic
|
||||
from beets.mediafile import MediaFile
|
||||
import headphones
|
||||
|
||||
blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]',
|
||||
'[traditional]', '[unknown]', 'Various Artists']
|
||||
'[traditional]', '[unknown]', 'Various Artists']
|
||||
blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088',
|
||||
'33cf029c-63b0-41a0-9855-be2a3665fb3b',
|
||||
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
|
||||
'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
|
||||
'9be7f096-97ec-4615-8957-8d40b5dcbc41',
|
||||
'125ec42a-7229-4250-afc5-e057484327fe',
|
||||
'89ad4ac3-39f7-470e-963a-56509c546377']
|
||||
'33cf029c-63b0-41a0-9855-be2a3665fb3b',
|
||||
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
|
||||
'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
|
||||
'9be7f096-97ec-4615-8957-8d40b5dcbc41',
|
||||
'125ec42a-7229-4250-afc5-e057484327fe',
|
||||
'89ad4ac3-39f7-470e-963a-56509c546377']
|
||||
|
||||
|
||||
def is_exists(artistid):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# See if the artist is already in the database
|
||||
artistlist = myDB.select('SELECT ArtistID, ArtistName from artists WHERE ArtistID=?', [artistid])
|
||||
artistlist = myDB.select('SELECT ArtistID, ArtistName from artists WHERE ArtistID=?',
|
||||
[artistid])
|
||||
|
||||
if any(artistid in x for x in artistlist):
|
||||
logger.info(artistlist[0][1] + u" is already in the database. Updating 'have tracks', but not artist information")
|
||||
logger.info(artistlist[0][
|
||||
1] + u" is already in the database. Updating 'have tracks', but not artist information")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def artistlist_to_mbids(artistlist, forced=False):
|
||||
|
||||
for artist in artistlist:
|
||||
|
||||
if not artist and artist != ' ':
|
||||
@@ -77,9 +77,11 @@ def artistlist_to_mbids(artistlist, forced=False):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
if not forced:
|
||||
bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?', [artistid]).fetchone()
|
||||
bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?',
|
||||
[artistid]).fetchone()
|
||||
if bl_artist or artistid in blacklisted_special_artists:
|
||||
logger.info("Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must do it manually (Artist ID: %s)" % (artist, artistid))
|
||||
logger.info("Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must "
|
||||
"do it manually (Artist ID: %s)" % (artist, artistid))
|
||||
continue
|
||||
|
||||
# Add to database if it doesn't exist
|
||||
@@ -88,7 +90,9 @@ def artistlist_to_mbids(artistlist, forced=False):
|
||||
|
||||
# Just update the tracks if it does
|
||||
else:
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
|
||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
|
||||
|
||||
# Delete it from the New Artists if the request came from there
|
||||
@@ -111,8 +115,7 @@ def addArtistIDListToDB(artistidlist):
|
||||
addArtisttoDB(artistid)
|
||||
|
||||
|
||||
def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
|
||||
def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
# Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums
|
||||
from headphones import cache
|
||||
|
||||
@@ -142,17 +145,24 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
"Status": "Loading",
|
||||
"IncludeExtras": headphones.CONFIG.INCLUDE_EXTRAS,
|
||||
"Extras": headphones.CONFIG.EXTRAS}
|
||||
if type == "series":
|
||||
newValueDict['Type'] = "series"
|
||||
else:
|
||||
newValueDict = {"Status": "Loading"}
|
||||
if dbartist["Type"] == "series":
|
||||
type = "series"
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
artist = mb.getArtist(artistid, extrasonly)
|
||||
if type == "series":
|
||||
artist = mb.getSeries(artistid)
|
||||
else:
|
||||
artist = mb.getArtist(artistid, extrasonly)
|
||||
|
||||
if artist and artist.get('artist_name') in blacklisted_special_artist_names:
|
||||
logger.warn('Cannot import blocked special purpose artist: %s' % artist.get('artist_name'))
|
||||
myDB.action('DELETE from artists WHERE ArtistID=?', [artistid])
|
||||
#in case it's already in the db
|
||||
# in case it's already in the db
|
||||
myDB.action('DELETE from albums WHERE ArtistID=?', [artistid])
|
||||
myDB.action('DELETE from tracks WHERE ArtistID=?', [artistid])
|
||||
return
|
||||
@@ -161,7 +171,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
logger.warn("Error fetching artist info. ID: " + artistid)
|
||||
if dbartist is None:
|
||||
newValueDict = {"ArtistName": "Fetch failed, try refreshing. (%s)" % (artistid),
|
||||
"Status": "Active"}
|
||||
"Status": "Active"}
|
||||
else:
|
||||
newValueDict = {"Status": "Active"}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
@@ -184,7 +194,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
# See if we need to grab extras. Artist specific extras take precedence
|
||||
# over global option. Global options are set when adding a new artist
|
||||
try:
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone()
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?',
|
||||
[artistid]).fetchone()
|
||||
includeExtras = db_artist['IncludeExtras']
|
||||
except IndexError:
|
||||
includeExtras = False
|
||||
@@ -199,9 +210,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
for groups in artist['releasegroups']:
|
||||
group_list.append(groups['id'])
|
||||
if not extrasonly:
|
||||
remove_missing_groups_from_albums = myDB.select("SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid])
|
||||
remove_missing_groups_from_albums = myDB.select(
|
||||
"SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid])
|
||||
else:
|
||||
remove_missing_groups_from_albums = myDB.select('SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', [artistid])
|
||||
remove_missing_groups_from_albums = myDB.select(
|
||||
'SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"',
|
||||
[artistid])
|
||||
for items in remove_missing_groups_from_albums:
|
||||
if items['AlbumID'] not in group_list:
|
||||
# Remove all from albums/tracks that aren't in release groups
|
||||
@@ -210,12 +224,15 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
myDB.action("DELETE FROM tracks WHERE AlbumID=?", [items['AlbumID']])
|
||||
myDB.action("DELETE FROM alltracks WHERE AlbumID=?", [items['AlbumID']])
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']])
|
||||
logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (artist['artist_name'], items['AlbumID']))
|
||||
logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (
|
||||
artist['artist_name'], items['AlbumID']))
|
||||
if not extrasonly:
|
||||
force_repackage = 1
|
||||
else:
|
||||
if not extrasonly:
|
||||
logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name'])
|
||||
logger.info(
|
||||
"[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" %
|
||||
artist['artist_name'])
|
||||
|
||||
# Then search for releases within releasegroups, if releases don't exist, then remove from allalbums/alltracks
|
||||
album_searches = []
|
||||
@@ -225,7 +242,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
today = helpers.today()
|
||||
rgid = rg['id']
|
||||
skip_log = 0
|
||||
#Make a user configurable variable to skip update of albums with release dates older than this date (in days)
|
||||
# Make a user configurable variable to skip update of albums with release dates older than this date (in days)
|
||||
pause_delta = headphones.CONFIG.MB_IGNORE_AGE
|
||||
|
||||
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
|
||||
@@ -257,20 +274,24 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
else:
|
||||
release_date = today
|
||||
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
|
||||
logger.info("[%s] Now updating: %s (Release Date <%s Days)", artist['artist_name'], rg['title'], pause_delta)
|
||||
logger.info("[%s] Now updating: %s (Release Date <%s Days)",
|
||||
artist['artist_name'], rg['title'], pause_delta)
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
||||
else:
|
||||
logger.info("[%s] Skipping: %s (Release Date >%s Days)", artist['artist_name'], rg['title'], pause_delta)
|
||||
logger.info("[%s] Skipping: %s (Release Date >%s Days)",
|
||||
artist['artist_name'], rg['title'], pause_delta)
|
||||
skip_log = 1
|
||||
new_releases = 0
|
||||
|
||||
if force_repackage == 1:
|
||||
new_releases = -1
|
||||
logger.info('[%s] Forcing repackage of %s (Release Group Removed)', artist['artist_name'], al_title)
|
||||
logger.info('[%s] Forcing repackage of %s (Release Group Removed)',
|
||||
artist['artist_name'], al_title)
|
||||
else:
|
||||
new_releases = new_releases
|
||||
else:
|
||||
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'], rg['title'])
|
||||
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'],
|
||||
rg['title'])
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras, forcefull)
|
||||
|
||||
if new_releases != 0:
|
||||
@@ -284,23 +305,26 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
# This will be used later to build a hybrid release
|
||||
fullreleaselist = []
|
||||
# Search for releases within a release group
|
||||
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']])
|
||||
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?",
|
||||
[rg['id']])
|
||||
|
||||
# Build the dictionary for the fullreleaselist
|
||||
for items in find_hybrid_releases:
|
||||
if items['ReleaseID'] != rg['id']: #don't include hybrid information, since that's what we're replacing
|
||||
# don't include hybrid information, since that's what we're replacing
|
||||
if items['ReleaseID'] != rg['id']:
|
||||
hybrid_release_id = items['ReleaseID']
|
||||
newValueDict = {"ArtistID": items['ArtistID'],
|
||||
"ArtistName": items['ArtistName'],
|
||||
"AlbumTitle": items['AlbumTitle'],
|
||||
"AlbumID": items['AlbumID'],
|
||||
"AlbumASIN": items['AlbumASIN'],
|
||||
"ReleaseDate": items['ReleaseDate'],
|
||||
"Type": items['Type'],
|
||||
"ReleaseCountry": items['ReleaseCountry'],
|
||||
"ReleaseFormat": items['ReleaseFormat']
|
||||
}
|
||||
find_hybrid_tracks = myDB.action("SELECT * from alltracks WHERE ReleaseID=?", [hybrid_release_id])
|
||||
"ArtistName": items['ArtistName'],
|
||||
"AlbumTitle": items['AlbumTitle'],
|
||||
"AlbumID": items['AlbumID'],
|
||||
"AlbumASIN": items['AlbumASIN'],
|
||||
"ReleaseDate": items['ReleaseDate'],
|
||||
"Type": items['Type'],
|
||||
"ReleaseCountry": items['ReleaseCountry'],
|
||||
"ReleaseFormat": items['ReleaseFormat']
|
||||
}
|
||||
find_hybrid_tracks = myDB.action("SELECT * from alltracks WHERE ReleaseID=?",
|
||||
[hybrid_release_id])
|
||||
totalTracks = 1
|
||||
hybrid_track_array = []
|
||||
for hybrid_tracks in find_hybrid_tracks:
|
||||
@@ -308,9 +332,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
'number': hybrid_tracks['TrackNumber'],
|
||||
'title': hybrid_tracks['TrackTitle'],
|
||||
'id': hybrid_tracks['TrackID'],
|
||||
#'url': hybrid_tracks['TrackURL'],
|
||||
# 'url': hybrid_tracks['TrackURL'],
|
||||
'duration': hybrid_tracks['TrackDuration']
|
||||
})
|
||||
})
|
||||
totalTracks += 1
|
||||
newValueDict['ReleaseID'] = hybrid_release_id
|
||||
newValueDict['Tracks'] = hybrid_track_array
|
||||
@@ -320,10 +344,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
# This may end up being called with an empty fullreleaselist
|
||||
try:
|
||||
hybridrelease = getHybridRelease(fullreleaselist)
|
||||
logger.info('[%s] Packaging %s releases into hybrid title' % (artist['artist_name'], rg['title']))
|
||||
logger.info('[%s] Packaging %s releases into hybrid title' % (
|
||||
artist['artist_name'], rg['title']))
|
||||
except Exception as e:
|
||||
errors = True
|
||||
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'], rg['title'], e))
|
||||
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (
|
||||
artist['artist_name'], rg['title'], e))
|
||||
continue
|
||||
|
||||
# Use the ReleaseGroupID as the ReleaseID for the hybrid release to differentiate it
|
||||
@@ -338,13 +364,13 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
"AlbumASIN": hybridrelease['AlbumASIN'],
|
||||
"ReleaseDate": hybridrelease['ReleaseDate'],
|
||||
"Type": rg['type']
|
||||
}
|
||||
}
|
||||
|
||||
myDB.upsert("allalbums", newValueDict, controlValueDict)
|
||||
|
||||
for track in hybridrelease['Tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
|
||||
cleanname = helpers.clean_name(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": rg['id']}
|
||||
@@ -358,25 +384,29 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
}
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?',
|
||||
[cleanname]).fetchone()
|
||||
|
||||
if not match:
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [artist['artist_name'], rg['title'], track['title']]).fetchone()
|
||||
#if not match:
|
||||
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[artist['artist_name'], rg['title'], track['title']]).fetchone()
|
||||
# if not match:
|
||||
# match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
if match:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (rg['id'], match['Location']))
|
||||
# myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?',
|
||||
(rg['id'], match['Location']))
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
# Delete matched tracks from the have table
|
||||
#myDB.action('DELETE from have WHERE Matched="True"')
|
||||
# myDB.action('DELETE from have WHERE Matched="True"')
|
||||
|
||||
# If there's no release in the main albums tables, add the default (hybrid)
|
||||
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
|
||||
@@ -401,7 +431,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
"Type": album['Type'],
|
||||
"ReleaseCountry": album['ReleaseCountry'],
|
||||
"ReleaseFormat": album['ReleaseFormat']
|
||||
}
|
||||
}
|
||||
|
||||
if rg_exists:
|
||||
newValueDict['DateAdded'] = rg_exists['DateAdded']
|
||||
@@ -418,14 +448,17 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
newValueDict['Status'] = "Wanted"
|
||||
# Sometimes "new" albums are added to musicbrainz after their release date, so let's try to catch these
|
||||
# The first test just makes sure we have year-month-day
|
||||
elif helpers.get_age(album['ReleaseDate']) and helpers.get_age(today) - helpers.get_age(album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING:
|
||||
elif helpers.get_age(album['ReleaseDate']) and helpers.get_age(
|
||||
today) - helpers.get_age(
|
||||
album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING:
|
||||
newValueDict['Status'] = "Wanted"
|
||||
else:
|
||||
newValueDict['Status'] = "Skipped"
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [releaseid]).fetchall()
|
||||
tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?',
|
||||
[releaseid]).fetchall()
|
||||
|
||||
# This is used to see how many tracks you have from an album - to
|
||||
# mark it as downloaded. Default is 80%, can be set in config as
|
||||
@@ -434,7 +467,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
|
||||
if total_track_count == 0:
|
||||
logger.warning("Total track count is zero for Release ID " +
|
||||
"'%s', skipping.", releaseid)
|
||||
"'%s', skipping.", releaseid)
|
||||
continue
|
||||
|
||||
for track in tracks:
|
||||
@@ -442,35 +475,43 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
"AlbumID": rg['id']}
|
||||
|
||||
newValueDict = {"ArtistID": track['ArtistID'],
|
||||
"ArtistName": track['ArtistName'],
|
||||
"AlbumTitle": track['AlbumTitle'],
|
||||
"AlbumASIN": track['AlbumASIN'],
|
||||
"ReleaseID": track['ReleaseID'],
|
||||
"TrackTitle": track['TrackTitle'],
|
||||
"TrackDuration": track['TrackDuration'],
|
||||
"TrackNumber": track['TrackNumber'],
|
||||
"CleanName": track['CleanName'],
|
||||
"Location": track['Location'],
|
||||
"Format": track['Format'],
|
||||
"BitRate": track['BitRate']
|
||||
}
|
||||
"ArtistName": track['ArtistName'],
|
||||
"AlbumTitle": track['AlbumTitle'],
|
||||
"AlbumASIN": track['AlbumASIN'],
|
||||
"ReleaseID": track['ReleaseID'],
|
||||
"TrackTitle": track['TrackTitle'],
|
||||
"TrackDuration": track['TrackDuration'],
|
||||
"TrackNumber": track['TrackNumber'],
|
||||
"CleanName": track['CleanName'],
|
||||
"Location": track['Location'],
|
||||
"Format": track['Format'],
|
||||
"BitRate": track['BitRate']
|
||||
}
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
|
||||
have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [rg['id']]))
|
||||
have_track_count = len(
|
||||
myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL',
|
||||
[rg['id']]))
|
||||
marked_as_downloaded = False
|
||||
|
||||
if rg_exists:
|
||||
if rg_exists['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']])
|
||||
if rg_exists['Status'] == 'Skipped' and (
|
||||
(have_track_count / float(total_track_count)) >= (
|
||||
headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?',
|
||||
['Downloaded', rg['id']])
|
||||
marked_as_downloaded = True
|
||||
else:
|
||||
if (have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']])
|
||||
if (have_track_count / float(total_track_count)) >= (
|
||||
headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?',
|
||||
['Downloaded', rg['id']])
|
||||
marked_as_downloaded = True
|
||||
|
||||
logger.info(u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
||||
logger.info(
|
||||
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
||||
cache.getThumb(AlbumID=rg['id'])
|
||||
|
||||
# Start a search for the album if it's new, hasn't been marked as
|
||||
@@ -480,7 +521,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
album_searches.append(rg['id'])
|
||||
else:
|
||||
if skip_log == 0:
|
||||
logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title']))
|
||||
logger.info(u"[%s] No new releases, so no changes made to %s" % (
|
||||
artist['artist_name'], rg['title']))
|
||||
|
||||
time.sleep(3)
|
||||
finalize_update(artistid, artist['artist_name'], errors)
|
||||
@@ -488,8 +530,13 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
||||
cache.getThumb(ArtistID=artistid)
|
||||
|
||||
logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name'])
|
||||
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
|
||||
|
||||
if errors:
|
||||
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
|
||||
logger.info(
|
||||
"[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (
|
||||
artist['artist_name'], artist['artist_name']))
|
||||
else:
|
||||
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
|
||||
logger.info(u"Updating complete for: %s" % artist['artist_name'])
|
||||
@@ -508,10 +555,18 @@ def finalize_update(artistid, artistname, errors=False):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [artistid]))
|
||||
#havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artistname]))
|
||||
latestalbum = myDB.action(
|
||||
'SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC',
|
||||
[artistid]).fetchone()
|
||||
totaltracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
||||
[artistid]))
|
||||
# havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[artistid])) + len(
|
||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[artistname]))
|
||||
|
||||
controlValueDict = {"ArtistID": artistid}
|
||||
|
||||
@@ -534,7 +589,6 @@ def finalize_update(artistid, artistname, errors=False):
|
||||
|
||||
|
||||
def addReleaseById(rid, rgid=None):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# Create minimum info upfront if added from searchresults
|
||||
@@ -553,14 +607,18 @@ def addReleaseById(rid, rgid=None):
|
||||
rgid = None
|
||||
artistid = None
|
||||
release_dict = None
|
||||
results = myDB.select("SELECT albums.ArtistID, releases.ReleaseGroupID from releases, albums WHERE releases.ReleaseID=? and releases.ReleaseGroupID=albums.AlbumID LIMIT 1", [rid])
|
||||
results = myDB.select(
|
||||
"SELECT albums.ArtistID, releases.ReleaseGroupID from releases, albums WHERE releases.ReleaseID=? and releases.ReleaseGroupID=albums.AlbumID LIMIT 1",
|
||||
[rid])
|
||||
for result in results:
|
||||
rgid = result['ReleaseGroupID']
|
||||
artistid = result['ArtistID']
|
||||
logger.debug("Found a cached releaseid : releasegroupid relationship: " + rid + " : " + rgid)
|
||||
logger.debug(
|
||||
"Found a cached releaseid : releasegroupid relationship: " + rid + " : " + rgid)
|
||||
if not rgid:
|
||||
#didn't find it in the cache, get the information from MB
|
||||
logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID")
|
||||
# didn't find it in the cache, get the information from MB
|
||||
logger.debug(
|
||||
"Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID")
|
||||
try:
|
||||
release_dict = mb.getRelease(rid)
|
||||
except Exception as e:
|
||||
@@ -577,10 +635,10 @@ def addReleaseById(rid, rgid=None):
|
||||
rgid = release_dict['rgid']
|
||||
artistid = release_dict['artist_id']
|
||||
|
||||
#we don't want to make more calls to MB here unless we have to, could be happening quite a lot
|
||||
# we don't want to make more calls to MB here unless we have to, could be happening quite a lot
|
||||
rg_exists = myDB.select("SELECT * from albums WHERE AlbumID=?", [rgid])
|
||||
|
||||
#make sure the artist exists since I don't know what happens later if it doesn't
|
||||
# make sure the artist exists since I don't know what happens later if it doesn't
|
||||
artist_exists = myDB.select("SELECT * from artists WHERE ArtistID=?", [artistid])
|
||||
|
||||
if not artist_exists and release_dict:
|
||||
@@ -589,7 +647,8 @@ def addReleaseById(rid, rgid=None):
|
||||
else:
|
||||
sortname = release_dict['artist_name']
|
||||
|
||||
logger.info(u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
|
||||
logger.info(
|
||||
u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
|
||||
controlValueDict = {"ArtistID": release_dict['artist_id']}
|
||||
newValueDict = {"ArtistName": release_dict['artist_name'],
|
||||
"ArtistSortName": sortname,
|
||||
@@ -614,13 +673,14 @@ def addReleaseById(rid, rgid=None):
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
elif not artist_exists and not release_dict:
|
||||
logger.error("Artist does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
logger.error(
|
||||
"Artist does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
if status == 'Loading':
|
||||
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
|
||||
return
|
||||
|
||||
if not rg_exists and release_dict or status == 'Loading' and release_dict: #it should never be the case that we have an rg and not the artist
|
||||
#but if it is this will fail
|
||||
if not rg_exists and release_dict or status == 'Loading' and release_dict: # it should never be the case that we have an rg and not the artist
|
||||
# but if it is this will fail
|
||||
logger.info(u"Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid)
|
||||
controlValueDict = {"AlbumID": rgid}
|
||||
if status != 'Loading':
|
||||
@@ -629,7 +689,8 @@ def addReleaseById(rid, rgid=None):
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ReleaseID": rgid,
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['title'] if 'title' in release_dict else release_dict['rg_title'],
|
||||
"AlbumTitle": release_dict['title'] if 'title' in release_dict else
|
||||
release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"ReleaseDate": release_dict['date'],
|
||||
"DateAdded": helpers.today(),
|
||||
@@ -640,41 +701,48 @@ def addReleaseById(rid, rgid=None):
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
#keep a local cache of these so that external programs that are adding releasesByID don't hammer MB
|
||||
# keep a local cache of these so that external programs that are adding releasesByID don't hammer MB
|
||||
myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']])
|
||||
|
||||
for track in release_dict['tracks']:
|
||||
cleanname = helpers.cleanName(release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
|
||||
cleanname = helpers.clean_name(
|
||||
release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"AlbumID": rgid}
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format, Matched from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format, Matched from have WHERE CleanName=?',
|
||||
[cleanname]).fetchone()
|
||||
|
||||
if not match:
|
||||
match = myDB.action('SELECT Location, BitRate, Format, Matched from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release_dict['artist_name'], release_dict['rg_title'], track['title']]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format, Matched from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[release_dict['artist_name'], release_dict['rg_title'],
|
||||
track['title']]).fetchone()
|
||||
|
||||
#if not match:
|
||||
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
# if not match:
|
||||
# match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
|
||||
if match:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('DELETE from have WHERE Location=?', [match['Location']])
|
||||
# myDB.action('DELETE from have WHERE Location=?', [match['Location']])
|
||||
|
||||
# If the album has been scanned before adding the release it will be unmatched, update to matched
|
||||
if match['Matched'] == 'Failed':
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release_dict['rgid'], match['Location']))
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?',
|
||||
(release_dict['rgid'], match['Location']))
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
@@ -693,7 +761,8 @@ def addReleaseById(rid, rgid=None):
|
||||
searcher.searchforalbum(rgid, False)
|
||||
|
||||
elif not rg_exists and not release_dict:
|
||||
logger.error("ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
logger.error(
|
||||
"ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
if status == 'Loading':
|
||||
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
|
||||
return
|
||||
@@ -801,11 +870,13 @@ def getHybridRelease(fullreleaselist):
|
||||
|
||||
sortable_release_list.sort(key=lambda x: getSortableReleaseDate(x['releasedate']))
|
||||
|
||||
average_tracks = sum(x['trackscount'] for x in sortable_release_list) / float(len(sortable_release_list))
|
||||
average_tracks = sum(x['trackscount'] for x in sortable_release_list) / float(
|
||||
len(sortable_release_list))
|
||||
for item in sortable_release_list:
|
||||
item['trackscount_delta'] = abs(average_tracks - item['trackscount'])
|
||||
|
||||
a = helpers.multikeysort(sortable_release_list, ['-hasasin', 'country', 'format', 'trackscount_delta'])
|
||||
a = helpers.multikeysort(sortable_release_list,
|
||||
['-hasasin', 'country', 'format', 'trackscount_delta'])
|
||||
|
||||
release_dict = {'ReleaseDate': sortable_release_list[0]['releasedate'],
|
||||
'Tracks': a[0]['tracks'],
|
||||
|
||||
@@ -14,15 +14,14 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import random
|
||||
import headphones
|
||||
import headphones.lock
|
||||
|
||||
from headphones import db, logger, request
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
TIMEOUT = 60.0 # seconds
|
||||
REQUEST_LIMIT = 1.0 / 5 # seconds
|
||||
import headphones
|
||||
import headphones.lock
|
||||
from headphones import db, logger, request
|
||||
|
||||
TIMEOUT = 60.0 # seconds
|
||||
REQUEST_LIMIT = 1.0 / 5 # seconds
|
||||
ENTRY_POINT = "http://ws.audioscrobbler.com/2.0/"
|
||||
API_KEY = "395e6ec6bb557382fc41fde867bce66f"
|
||||
|
||||
@@ -79,7 +78,7 @@ def getSimilar():
|
||||
try:
|
||||
artist_mbid = artist["mbid"]
|
||||
artist_name = artist["name"]
|
||||
except TypeError:
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if not any(artist_mbid in x for x in results):
|
||||
@@ -116,7 +115,7 @@ def getArtists():
|
||||
return
|
||||
|
||||
logger.info("Fetching artists from Last.FM for username: %s", headphones.CONFIG.LASTFM_USERNAME)
|
||||
data = request_lastfm("library.getartists", limit=10000, user=headphones.CONFIG.LASTFM_USERNAME)
|
||||
data = request_lastfm("library.getartists", limit=1000, user=headphones.CONFIG.LASTFM_USERNAME)
|
||||
|
||||
if data and "artists" in data:
|
||||
artistlist = []
|
||||
|
||||
+132
-85
@@ -14,17 +14,17 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import math
|
||||
|
||||
import headphones
|
||||
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
|
||||
from headphones import db, logger, helpers, importer, lastfm
|
||||
|
||||
|
||||
# You can scan a single directory and append it to the current library by
|
||||
# specifying append=True, ArtistID and ArtistName.
|
||||
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
cron=False):
|
||||
|
||||
cron=False, artistScan=False):
|
||||
if cron and not headphones.CONFIG.LIBRARYSCAN:
|
||||
return
|
||||
|
||||
@@ -36,11 +36,12 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
# If we're appending a dir, it's coming from the post processor which is
|
||||
# already bytestring
|
||||
if not append:
|
||||
if not append or artistScan:
|
||||
dir = dir.encode(headphones.SYS_ENCODING)
|
||||
|
||||
if not os.path.isdir(dir):
|
||||
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING,
|
||||
'replace'))
|
||||
return
|
||||
|
||||
myDB = db.DBConnection()
|
||||
@@ -50,13 +51,16 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
if not append:
|
||||
# Clean up bad filepaths
|
||||
tracks = myDB.select('SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
|
||||
tracks = myDB.select(
|
||||
'SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
|
||||
|
||||
for track in tracks:
|
||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING)
|
||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
||||
if not os.path.isfile(encoded_track_string):
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, track['Location']])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, track['Location']])
|
||||
|
||||
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
|
||||
|
||||
@@ -67,7 +71,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
# Make sure deleted files get accounted for when updating artist track counts
|
||||
new_artists.append(track['ArtistName'])
|
||||
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
||||
logger.info('File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
bitrates = []
|
||||
song_list = []
|
||||
@@ -89,9 +95,14 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
latest_subdirectory.append(subdirectory)
|
||||
|
||||
if file_count == 0 and r.replace(dir, '') != '':
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
elif latest_subdirectory[file_count] != latest_subdirectory[file_count - 1] and file_count != 0:
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (
|
||||
dir.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
elif latest_subdirectory[file_count] != latest_subdirectory[
|
||||
file_count - 1] and file_count != 0:
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (
|
||||
dir.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
|
||||
song = os.path.join(r, files)
|
||||
|
||||
@@ -102,10 +113,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
try:
|
||||
f = MediaFile(song)
|
||||
except (FileTypeError, UnreadableFileError):
|
||||
logger.warning("Cannot read media file '%s', skipping. It may be corrupted or not a media file.", unicode_song_path)
|
||||
logger.warning(
|
||||
"Cannot read media file '%s', skipping. It may be corrupted or not a media file.",
|
||||
unicode_song_path)
|
||||
continue
|
||||
except IOError:
|
||||
logger.warning("Cannnot read media file '%s', skipping. Does the file exists?", unicode_song_path)
|
||||
logger.warning("Cannnot read media file '%s', skipping. Does the file exists?",
|
||||
unicode_song_path)
|
||||
continue
|
||||
|
||||
# Grab the bitrates for the auto detect bit rate option
|
||||
@@ -124,52 +138,59 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
# TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements)
|
||||
|
||||
if f_artist and f.album and f.title:
|
||||
CleanName = helpers.cleanName(f_artist + ' ' + f.album + ' ' + f.title)
|
||||
CleanName = helpers.clean_name(f_artist + ' ' + f.album + ' ' + f.title)
|
||||
else:
|
||||
CleanName = None
|
||||
|
||||
controlValueDict = {'Location': unicode_song_path}
|
||||
|
||||
newValueDict = {'TrackID': f.mb_trackid,
|
||||
#'ReleaseID' : f.mb_albumid,
|
||||
'ArtistName': f_artist,
|
||||
'AlbumTitle': f.album,
|
||||
'TrackNumber': f.track,
|
||||
'TrackLength': f.length,
|
||||
'Genre': f.genre,
|
||||
'Date': f.date,
|
||||
'TrackTitle': f.title,
|
||||
'BitRate': f.bitrate,
|
||||
'Format': f.format,
|
||||
'CleanName': CleanName
|
||||
}
|
||||
# 'ReleaseID' : f.mb_albumid,
|
||||
'ArtistName': f_artist,
|
||||
'AlbumTitle': f.album,
|
||||
'TrackNumber': f.track,
|
||||
'TrackLength': f.length,
|
||||
'Genre': f.genre,
|
||||
'Date': f.date,
|
||||
'TrackTitle': f.title,
|
||||
'BitRate': f.bitrate,
|
||||
'Format': f.format,
|
||||
'CleanName': CleanName
|
||||
}
|
||||
|
||||
#song_list.append(song_dict)
|
||||
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone()
|
||||
#Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
|
||||
# song_list.append(song_dict)
|
||||
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?",
|
||||
[unicode_song_path]).fetchone()
|
||||
# Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
|
||||
if not check_exist_song:
|
||||
#This is a new track
|
||||
# This is a new track
|
||||
if f_artist:
|
||||
new_artists.append(f_artist)
|
||||
myDB.upsert("have", newValueDict, controlValueDict)
|
||||
new_song_count += 1
|
||||
else:
|
||||
if check_exist_song['ArtistName'] != f_artist or check_exist_song['AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
|
||||
#Important track metadata has been modified, need to run matcher again
|
||||
if check_exist_song['ArtistName'] != f_artist or check_exist_song[
|
||||
'AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
|
||||
# Important track metadata has been modified, need to run matcher again
|
||||
if f_artist and f_artist != check_exist_song['ArtistName']:
|
||||
new_artists.append(f_artist)
|
||||
elif f_artist and f_artist == check_exist_song['ArtistName'] and check_exist_song['Matched'] != "Ignored":
|
||||
elif f_artist and f_artist == check_exist_song['ArtistName'] and \
|
||||
check_exist_song['Matched'] != "Ignored":
|
||||
new_artists.append(f_artist)
|
||||
else:
|
||||
continue
|
||||
|
||||
newValueDict['Matched'] = None
|
||||
myDB.upsert("have", newValueDict, controlValueDict)
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path])
|
||||
myDB.action(
|
||||
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, unicode_song_path])
|
||||
myDB.action(
|
||||
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, unicode_song_path])
|
||||
new_song_count += 1
|
||||
else:
|
||||
#This track information hasn't changed
|
||||
# This track information hasn't changed
|
||||
if f_artist and check_exist_song['Matched'] != "Ignored":
|
||||
new_artists.append(f_artist)
|
||||
|
||||
@@ -177,19 +198,24 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
# Now we start track matching
|
||||
logger.info("%s new/modified songs found and added to the database" % new_song_count)
|
||||
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"])
|
||||
total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0]
|
||||
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
|
||||
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
||||
[dir.decode(headphones.SYS_ENCODING, 'replace') + "%"])
|
||||
total_number_of_songs = \
|
||||
myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
||||
[dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0]
|
||||
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(
|
||||
headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
|
||||
|
||||
# Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid)
|
||||
# When we insert into the database, the tracks with the most specific information will overwrite the more general matches
|
||||
|
||||
##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
|
||||
# song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
|
||||
song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle'])
|
||||
|
||||
# We'll use this to give a % completion, just because the track matching might take a while
|
||||
song_count = 0
|
||||
latest_artist = []
|
||||
last_completion_percentage = 0
|
||||
|
||||
for song in song_list:
|
||||
|
||||
@@ -200,27 +226,30 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
logger.info("Now matching songs by %s" % song['ArtistName'])
|
||||
|
||||
song_count += 1
|
||||
completion_percentage = float(song_count) / total_number_of_songs * 100
|
||||
completion_percentage = math.floor(float(song_count) / total_number_of_songs * 1000) / 10
|
||||
|
||||
if completion_percentage % 10 == 0:
|
||||
if completion_percentage >= (last_completion_percentage + 10):
|
||||
logger.info("Track matching is " + str(completion_percentage) + "% complete")
|
||||
last_completion_percentage = completion_percentage
|
||||
|
||||
#THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT
|
||||
#EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION,
|
||||
#ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
|
||||
#(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
|
||||
# THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT
|
||||
# EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION,
|
||||
# ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
|
||||
# (IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
|
||||
|
||||
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
|
||||
|
||||
track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
track = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
have_updated = False
|
||||
if track:
|
||||
controlValueDict = {'ArtistName': track['ArtistName'],
|
||||
'AlbumTitle': track['AlbumTitle'],
|
||||
'TrackTitle': track['TrackTitle']}
|
||||
'AlbumTitle': track['AlbumTitle'],
|
||||
'TrackTitle': track['TrackTitle']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
@@ -228,12 +257,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
else:
|
||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
|
||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?',
|
||||
[song['CleanName']]).fetchone()
|
||||
if track:
|
||||
controlValueDict = {'CleanName': track['CleanName']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
@@ -246,26 +276,30 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
|
||||
alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
alltrack = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = {'ArtistName': alltrack['ArtistName'],
|
||||
'AlbumTitle': alltrack['AlbumTitle'],
|
||||
'TrackTitle': alltrack['TrackTitle']}
|
||||
'AlbumTitle': alltrack['AlbumTitle'],
|
||||
'TrackTitle': alltrack['TrackTitle']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': alltrack['AlbumID']}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
else:
|
||||
alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
|
||||
alltrack = myDB.action(
|
||||
'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?',
|
||||
[song['CleanName']]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = {'CleanName': alltrack['CleanName']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
@@ -283,40 +317,44 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
newValueDict2 = {'Matched': "Failed"}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
|
||||
#######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
|
||||
# myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
|
||||
|
||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING,
|
||||
'replace'))
|
||||
|
||||
if not append:
|
||||
if not append or artistScan:
|
||||
logger.info('Updating scanned artist track counts')
|
||||
|
||||
# Clean up the new artist list
|
||||
unique_artists = {}.fromkeys(new_artists).keys()
|
||||
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
|
||||
|
||||
#There was a bug where artists with special characters (-,') would show up in new artists.
|
||||
# There was a bug where artists with special characters (-,') would show up in new artists.
|
||||
artist_list = [
|
||||
x for x in unique_artists
|
||||
if helpers.cleanName(x).lower() not in [
|
||||
helpers.cleanName(y[0]).lower()
|
||||
if helpers.clean_name(x).lower() not in [
|
||||
helpers.clean_name(y[0]).lower()
|
||||
for y in current_artists
|
||||
]
|
||||
]
|
||||
]
|
||||
artists_checked = [
|
||||
x for x in unique_artists
|
||||
if helpers.cleanName(x).lower() in [
|
||||
helpers.cleanName(y[0]).lower()
|
||||
if helpers.clean_name(x).lower() in [
|
||||
helpers.clean_name(y[0]).lower()
|
||||
for y in current_artists
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
# Update track counts
|
||||
for artist in artists_checked:
|
||||
# Have tracks are selected from tracks table and not all tracks because of duplicates
|
||||
# We update the track count upon an album switch to compliment this
|
||||
havetracks = (
|
||||
len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist]))
|
||||
+ len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist]))
|
||||
len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
|
||||
[artist])) + len(myDB.select(
|
||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[artist]))
|
||||
)
|
||||
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
|
||||
# (can fix by getting rid of second len statement)
|
||||
@@ -330,38 +368,47 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
importer.artistlist_to_mbids(artist_list)
|
||||
else:
|
||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||
#myDB.action('DELETE from newartists')
|
||||
# myDB.action('DELETE from newartists')
|
||||
for artist in artist_list:
|
||||
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
|
||||
|
||||
if headphones.CONFIG.DETECT_BITRATE:
|
||||
if headphones.CONFIG.DETECT_BITRATE and bitrates:
|
||||
headphones.CONFIG.PREFERRED_BITRATE = sum(bitrates) / len(bitrates) / 1000
|
||||
|
||||
else:
|
||||
# If we're appending a new album to the database, update the artists total track counts
|
||||
logger.info('Updating artist track counts')
|
||||
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName]))
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[ArtistID])) + len(myDB.select(
|
||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[ArtistName]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||
|
||||
if not append:
|
||||
update_album_status()
|
||||
|
||||
if not append and not artistScan:
|
||||
lastfm.getSimilar()
|
||||
|
||||
logger.info('Library scan complete')
|
||||
|
||||
#ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
|
||||
|
||||
# ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
|
||||
|
||||
|
||||
def update_album_status(AlbumID=None):
|
||||
myDB = db.DBConnection()
|
||||
logger.info('Counting matched tracks to mark albums as skipped/downloaded')
|
||||
if AlbumID:
|
||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID])
|
||||
album_status_updater = myDB.action(
|
||||
'SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID])
|
||||
else:
|
||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums')
|
||||
for album in album_status_updater:
|
||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?', [album['AlbumID']])
|
||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?',
|
||||
[album['AlbumID']])
|
||||
total_tracks = 0
|
||||
have_tracks = 0
|
||||
for track in track_counter:
|
||||
@@ -374,14 +421,14 @@ def update_album_status(AlbumID=None):
|
||||
album_completion = 0
|
||||
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
|
||||
|
||||
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped':
|
||||
if album_completion >= headphones.CONFIG.ALBUM_COMPLETION_PCT:
|
||||
new_album_status = "Downloaded"
|
||||
|
||||
# I don't think we want to change Downloaded->Skipped.....
|
||||
# I think we can only automatically change Skipped->Downloaded when updating
|
||||
# There was a bug report where this was causing infinite downloads if the album was
|
||||
# recent, but matched to less than 80%. It would go Downloaded->Skipped->Wanted->Downloaded->Skipped->Wanted->etc....
|
||||
#else:
|
||||
# else:
|
||||
# if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
|
||||
# new_album_status = "Skipped"
|
||||
# else:
|
||||
|
||||
+2
-1
@@ -2,11 +2,12 @@
|
||||
Locking-related classes
|
||||
"""
|
||||
|
||||
import headphones.logger
|
||||
import time
|
||||
import threading
|
||||
import Queue
|
||||
|
||||
import headphones.logger
|
||||
|
||||
|
||||
class TimedLock(object):
|
||||
"""
|
||||
|
||||
+17
-11
@@ -13,24 +13,24 @@
|
||||
# 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 helpers
|
||||
|
||||
from logutils.queue import QueueHandler, QueueListener
|
||||
from logging import handlers
|
||||
|
||||
import multiprocessing
|
||||
import contextlib
|
||||
import headphones
|
||||
import threading
|
||||
import traceback
|
||||
import logging
|
||||
import errno
|
||||
import sys
|
||||
|
||||
import os
|
||||
from headphones import helpers
|
||||
from logutils.queue import QueueHandler, QueueListener
|
||||
import headphones
|
||||
|
||||
|
||||
# These settings are for file logging only
|
||||
FILENAME = "headphones.log"
|
||||
MAX_SIZE = 1000000 # 1 MB
|
||||
MAX_SIZE = 1000000 # 1 MB
|
||||
MAX_FILES = 5
|
||||
|
||||
# Headphones logger
|
||||
@@ -39,6 +39,7 @@ logger = logging.getLogger("headphones")
|
||||
# Global queue for multiprocessing logging
|
||||
queue = None
|
||||
|
||||
|
||||
class LogListHandler(logging.Handler):
|
||||
"""
|
||||
Log handler for Web UI.
|
||||
@@ -71,8 +72,8 @@ def listener():
|
||||
# http://stackoverflow.com/questions/2009278 for more information.
|
||||
if e.errno == errno.EACCES:
|
||||
logger.warning("Multiprocess logging disabled, because "
|
||||
"current user cannot map shared memory. You won't see any" \
|
||||
"logging generated by the worker processed.")
|
||||
"current user cannot map shared memory. You won't see any"
|
||||
"logging generated by the worker processed.")
|
||||
|
||||
# Multiprocess logging may be disabled.
|
||||
if not queue:
|
||||
@@ -149,8 +150,10 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||
if log_dir:
|
||||
filename = os.path.join(log_dir, FILENAME)
|
||||
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES)
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE,
|
||||
backupCount=MAX_FILES)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
@@ -158,7 +161,8 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||
|
||||
# Setup console logger
|
||||
if console:
|
||||
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(console_formatter)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
@@ -212,11 +216,13 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
|
||||
raise
|
||||
except:
|
||||
excepthook(*sys.exc_info())
|
||||
|
||||
self.run = new_run
|
||||
|
||||
# Monkey patch the run() by monkey patching the __init__ method
|
||||
threading.Thread.__init__ = new_init
|
||||
|
||||
|
||||
# Expose logger methods
|
||||
info = logger.info
|
||||
warn = logger.warn
|
||||
|
||||
+15
-13
@@ -13,18 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import htmlentitydefs
|
||||
|
||||
import re
|
||||
from headphones import logger, request
|
||||
|
||||
|
||||
def getLyrics(artist, song):
|
||||
|
||||
params = {"artist": artist.encode('utf-8'),
|
||||
"song": song.encode('utf-8'),
|
||||
"fmt": 'xml'
|
||||
}
|
||||
"song": song.encode('utf-8'),
|
||||
"fmt": 'xml'
|
||||
}
|
||||
|
||||
url = 'http://lyrics.wikia.com/api.php'
|
||||
data = request.request_minidom(url, params=params)
|
||||
@@ -46,10 +45,13 @@ def getLyrics(artist, song):
|
||||
logger.warn('Error fetching lyrics from: %s' % lyricsurl)
|
||||
return
|
||||
|
||||
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage)
|
||||
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(
|
||||
lyricspage)
|
||||
|
||||
if not m:
|
||||
m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage)
|
||||
m = re.compile(
|
||||
'''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(
|
||||
lyricspage)
|
||||
if m:
|
||||
return u'(Instrumental)'
|
||||
else:
|
||||
@@ -67,12 +69,12 @@ def convert_html_entities(s):
|
||||
if len(matches) > 0:
|
||||
hits = set(matches)
|
||||
for hit in hits:
|
||||
name = hit[2:-1]
|
||||
try:
|
||||
entnum = int(name)
|
||||
s = s.replace(hit, unichr(entnum))
|
||||
except ValueError:
|
||||
pass
|
||||
name = hit[2:-1]
|
||||
try:
|
||||
entnum = int(name)
|
||||
s = s.replace(hit, unichr(entnum))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
matches = re.findall("&\w+;", s)
|
||||
hits = set(matches)
|
||||
|
||||
+254
-141
@@ -31,12 +31,12 @@ except ImportError:
|
||||
|
||||
mb_lock = headphones.lock.TimedLock(0)
|
||||
|
||||
|
||||
# Quick fix to add mirror switching on the fly. Need to probably return the mbhost & mbport that's
|
||||
# being used, so we can send those values to the log
|
||||
|
||||
|
||||
def startmb():
|
||||
|
||||
mbuser = None
|
||||
mbpass = None
|
||||
|
||||
@@ -47,10 +47,12 @@ def startmb():
|
||||
elif headphones.CONFIG.MIRROR == "custom":
|
||||
mbhost = headphones.CONFIG.CUSTOMHOST
|
||||
mbport = int(headphones.CONFIG.CUSTOMPORT)
|
||||
mbuser = headphones.CONFIG.CUSTOMUSER
|
||||
mbpass = headphones.CONFIG.CUSTOMPASS
|
||||
sleepytime = int(headphones.CONFIG.CUSTOMSLEEP)
|
||||
elif headphones.CONFIG.MIRROR == "headphones":
|
||||
mbhost = "144.76.94.239"
|
||||
mbport = 8181
|
||||
mbhost = "musicbrainz.codeshy.com"
|
||||
mbport = 80
|
||||
mbuser = headphones.CONFIG.HPUSER
|
||||
mbpass = headphones.CONFIG.HPPASS
|
||||
sleepytime = 0
|
||||
@@ -64,70 +66,82 @@ def startmb():
|
||||
if sleepytime == 0:
|
||||
musicbrainzngs.set_rate_limit(False)
|
||||
else:
|
||||
#calling it with an it ends up blocking all requests after the first
|
||||
# calling it with an it ends up blocking all requests after the first
|
||||
musicbrainzngs.set_rate_limit(limit_or_interval=float(sleepytime))
|
||||
mb_lock.minimum_delta = sleepytime
|
||||
|
||||
# Add headphones credentials
|
||||
if headphones.CONFIG.MIRROR == "headphones":
|
||||
if not mbuser and mbpass:
|
||||
logger.warn("No username or password set for VIP server")
|
||||
if headphones.CONFIG.MIRROR == "headphones" or headphones.CONFIG.CUSTOMAUTH:
|
||||
if not mbuser or not mbpass:
|
||||
logger.warn("No username or password set for MusicBrainz server")
|
||||
else:
|
||||
musicbrainzngs.hpauth(mbuser, mbpass)
|
||||
|
||||
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
|
||||
# Let us know if we disable custom authentication
|
||||
if not headphones.CONFIG.CUSTOMAUTH and headphones.CONFIG.MIRROR == "custom":
|
||||
musicbrainzngs.disable_hpauth()
|
||||
|
||||
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i',
|
||||
mbhost, mbport, sleepytime)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def findArtist(name, limit=1):
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
criteria = {'artist': name.lower()}
|
||||
|
||||
criteria = {'artist': name.lower()}
|
||||
with mb_lock:
|
||||
try:
|
||||
artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
|
||||
except ValueError as e:
|
||||
if "at least one query term is required" in e.message:
|
||||
logger.error(
|
||||
"Tried to search without a term, or an empty one. Provided artist (probably emtpy): %s",
|
||||
name)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not artistResults:
|
||||
return False
|
||||
for result in artistResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['sort-name'])
|
||||
if result['name'] != uniquename and limit == 1:
|
||||
logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name)
|
||||
artistdict = findArtistbyAlbum(name)
|
||||
if not artistdict:
|
||||
logger.info('Cannot determine the best match from an artist/album search. Using top match instead')
|
||||
artistlist.append({
|
||||
# Just need the artist id if the limit is 1
|
||||
# 'name': unicode(result['sort-name']),
|
||||
# 'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
# 'score': int(result['ext:score'])
|
||||
})
|
||||
else:
|
||||
artistlist.append(artistdict)
|
||||
else:
|
||||
artistlist.append({
|
||||
'name': unicode(result['sort-name']),
|
||||
'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return artistlist
|
||||
if not artistResults:
|
||||
return False
|
||||
for result in artistResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['sort-name'])
|
||||
if result['name'] != uniquename and limit == 1:
|
||||
logger.info(
|
||||
'Found an artist with a disambiguation: %s - doing an album based search' % name)
|
||||
artistdict = findArtistbyAlbum(name)
|
||||
if not artistdict:
|
||||
logger.info(
|
||||
'Cannot determine the best match from an artist/album search. Using top match instead')
|
||||
artistlist.append({
|
||||
# Just need the artist id if the limit is 1
|
||||
# 'name': unicode(result['sort-name']),
|
||||
# 'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
# 'score': int(result['ext:score'])
|
||||
})
|
||||
else:
|
||||
artistlist.append(artistdict)
|
||||
else:
|
||||
artistlist.append({
|
||||
'name': unicode(result['sort-name']),
|
||||
'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/artist/" + result['id']),
|
||||
# probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return artistlist
|
||||
|
||||
|
||||
def findRelease(name, limit=1, artist=None):
|
||||
@@ -138,16 +152,14 @@ def findRelease(name, limit=1, artist=None):
|
||||
if not artist and ':' in name:
|
||||
name, artist = name.rsplit(":", 1)
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
if artist and any((c in chars) for c in artist):
|
||||
artist = '"' + artist + '"'
|
||||
criteria = {'release': name.lower()}
|
||||
if artist:
|
||||
criteria['artist'] = artist.lower()
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)['release-list']
|
||||
except musicbrainzngs.WebServiceError as e: #need to update exceptions
|
||||
releaseResults = musicbrainzngs.search_releases(limit=limit, **criteria)['release-list']
|
||||
except musicbrainzngs.WebServiceError as e: # need to update exceptions
|
||||
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
@@ -191,28 +203,63 @@ def findRelease(name, limit=1, artist=None):
|
||||
rg_type = secondary_type
|
||||
|
||||
releaselist.append({
|
||||
'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
|
||||
'title': unicode(title),
|
||||
'id': unicode(result['artist-credit'][0]['artist']['id']),
|
||||
'albumid': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),#probably needs to be changed
|
||||
'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),#probably needs to be changed
|
||||
'score': int(result['ext:score']),
|
||||
'date': unicode(result['date']) if 'date' in result else '',
|
||||
'country': unicode(result['country']) if 'country' in result else '',
|
||||
'formats': unicode(formats),
|
||||
'tracks': unicode(tracks),
|
||||
'rgid': unicode(result['release-group']['id']),
|
||||
'rgtype': unicode(rg_type)
|
||||
})
|
||||
'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
|
||||
'title': unicode(title),
|
||||
'id': unicode(result['artist-credit'][0]['artist']['id']),
|
||||
'albumid': unicode(result['id']),
|
||||
'url': unicode(
|
||||
"http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),
|
||||
# probably needs to be changed
|
||||
'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),
|
||||
# probably needs to be changed
|
||||
'score': int(result['ext:score']),
|
||||
'date': unicode(result['date']) if 'date' in result else '',
|
||||
'country': unicode(result['country']) if 'country' in result else '',
|
||||
'formats': unicode(formats),
|
||||
'tracks': unicode(tracks),
|
||||
'rgid': unicode(result['release-group']['id']),
|
||||
'rgtype': unicode(rg_type)
|
||||
})
|
||||
return releaselist
|
||||
|
||||
|
||||
def findSeries(name, limit=1):
|
||||
serieslist = []
|
||||
seriesResults = None
|
||||
|
||||
criteria = {'series': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
seriesResults = musicbrainzngs.search_series(limit=limit, **criteria)['series-list']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not seriesResults:
|
||||
return False
|
||||
for result in seriesResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['name'])
|
||||
serieslist.append({
|
||||
'uniquename': uniquename,
|
||||
'name': unicode(result['name']),
|
||||
'type': unicode(result['type']),
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/series/" + result['id']),
|
||||
# probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return serieslist
|
||||
|
||||
|
||||
def getArtist(artistid, extrasonly=False):
|
||||
artist_dict = {}
|
||||
artist = None
|
||||
try:
|
||||
limit = 200
|
||||
limit = 100
|
||||
with mb_lock:
|
||||
artist = musicbrainzngs.get_artist_by_id(artistid)['artist']
|
||||
newRgs = None
|
||||
@@ -227,7 +274,9 @@ def getArtist(artistid, extrasonly=False):
|
||||
newRgs = newRgs['release-group-list']
|
||||
artist['release-group-list'] += newRgs
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (
|
||||
artistid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
except Exception as e:
|
||||
pass
|
||||
@@ -241,7 +290,7 @@ def getArtist(artistid, extrasonly=False):
|
||||
|
||||
if not extrasonly:
|
||||
for rg in artist['release-group-list']:
|
||||
if "secondary-type-list" in rg.keys(): #only add releases without a secondary type
|
||||
if "secondary-type-list" in rg.keys(): # only add releases without a secondary type
|
||||
continue
|
||||
releasegroups.append({
|
||||
'title': unicode(rg['title']),
|
||||
@@ -255,7 +304,8 @@ def getArtist(artistid, extrasonly=False):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
try:
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone()
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?',
|
||||
[artistid]).fetchone()
|
||||
includeExtras = db_artist['IncludeExtras']
|
||||
except IndexError:
|
||||
includeExtras = False
|
||||
@@ -282,7 +332,7 @@ def getArtist(artistid, extrasonly=False):
|
||||
mb_extras_list = []
|
||||
|
||||
try:
|
||||
limit = 200
|
||||
limit = 100
|
||||
newRgs = None
|
||||
while newRgs is None or len(newRgs) >= limit:
|
||||
with mb_lock:
|
||||
@@ -291,7 +341,9 @@ def getArtist(artistid, extrasonly=False):
|
||||
newRgs = newRgs['release-group-list']
|
||||
mb_extras_list += newRgs
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (
|
||||
artistid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
for rg in mb_extras_list:
|
||||
@@ -311,6 +363,44 @@ def getArtist(artistid, extrasonly=False):
|
||||
return artist_dict
|
||||
|
||||
|
||||
def getSeries(seriesid):
|
||||
series_dict = {}
|
||||
series = None
|
||||
try:
|
||||
with mb_lock:
|
||||
series = musicbrainzngs.get_series_by_id(seriesid, includes=['release-group-rels'])[
|
||||
'series']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn(
|
||||
'Attempt to retrieve series information from MusicBrainz failed for seriesid: %s (%s)' % (
|
||||
seriesid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not series:
|
||||
return False
|
||||
|
||||
if 'disambiguation' in series:
|
||||
series_dict['artist_name'] = unicode(
|
||||
series['name'] + " (" + unicode(series['disambiguation']) + ")")
|
||||
else:
|
||||
series_dict['artist_name'] = unicode(series['name'])
|
||||
|
||||
releasegroups = []
|
||||
|
||||
for rg in series['release_group-relation-list']:
|
||||
releasegroup = rg['release-group']
|
||||
releasegroups.append({
|
||||
'title': releasegroup['title'],
|
||||
'date': releasegroup['first-release-date'],
|
||||
'id': releasegroup['id'],
|
||||
'type': rg['type']
|
||||
})
|
||||
series_dict['releasegroups'] = releasegroups
|
||||
return series_dict
|
||||
|
||||
|
||||
def getReleaseGroup(rgid):
|
||||
"""
|
||||
Returns a list of releases in a release group
|
||||
@@ -322,7 +412,9 @@ def getReleaseGroup(rgid):
|
||||
rgid, ["artists", "releases", "media", "discids", ])
|
||||
releaseGroup = releaseGroup['release-group']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||
rgid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not releaseGroup:
|
||||
@@ -341,11 +433,16 @@ def getRelease(releaseid, include_artist_info=True):
|
||||
try:
|
||||
with mb_lock:
|
||||
if include_artist_info:
|
||||
results = musicbrainzngs.get_release_by_id(releaseid, ["artists", "release-groups", "media", "recordings"]).get('release')
|
||||
results = musicbrainzngs.get_release_by_id(releaseid,
|
||||
["artists", "release-groups", "media",
|
||||
"recordings"]).get('release')
|
||||
else:
|
||||
results = musicbrainzngs.get_release_by_id(releaseid, ["media", "recordings"]).get('release')
|
||||
results = musicbrainzngs.get_release_by_id(releaseid, ["media", "recordings"]).get(
|
||||
'release')
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (
|
||||
releaseid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not results:
|
||||
@@ -373,7 +470,8 @@ def getRelease(releaseid, include_artist_info=True):
|
||||
try:
|
||||
release['rg_type'] = unicode(results['release-group']['type'])
|
||||
|
||||
if release['rg_type'] == 'Album' and 'secondary-type-list' in results['release-group']:
|
||||
if release['rg_type'] == 'Album' and 'secondary-type-list' in results[
|
||||
'release-group']:
|
||||
secondary_type = unicode(results['release-group']['secondary-type-list'][0])
|
||||
if secondary_type != release['rg_type']:
|
||||
release['rg_type'] = secondary_type
|
||||
@@ -393,9 +491,13 @@ def getRelease(releaseid, include_artist_info=True):
|
||||
|
||||
|
||||
def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
results = []
|
||||
|
||||
release_status = "official"
|
||||
if includeExtras and not headphones.CONFIG.OFFICIAL_RELEASES_ONLY:
|
||||
release_status = []
|
||||
|
||||
try:
|
||||
limit = 100
|
||||
newResults = None
|
||||
@@ -404,29 +506,33 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
newResults = musicbrainzngs.browse_releases(
|
||||
release_group=rgid,
|
||||
includes=['artist-credits', 'labels', 'recordings', 'release-groups', 'media'],
|
||||
release_status=release_status,
|
||||
limit=limit,
|
||||
offset=len(results))
|
||||
if 'release-list' not in newResults:
|
||||
break #may want to raise an exception here instead ?
|
||||
break # may want to raise an exception here instead ?
|
||||
newResults = newResults['release-list']
|
||||
results += newResults
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||
rgid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
return False
|
||||
|
||||
if not results or len(results) == 0:
|
||||
return False
|
||||
|
||||
#Clean all references to releases in dB that are no longer referenced in musicbrainz
|
||||
# Clean all references to releases in dB that are no longer referenced in musicbrainz
|
||||
release_list = []
|
||||
force_repackage1 = 0
|
||||
if len(results) != 0:
|
||||
for release_mark in results:
|
||||
release_list.append(unicode(release_mark['id']))
|
||||
release_title = release_mark['title']
|
||||
remove_missing_releases = myDB.action("SELECT ReleaseID FROM allalbums WHERE AlbumID=?", [rgid])
|
||||
remove_missing_releases = myDB.action("SELECT ReleaseID FROM allalbums WHERE AlbumID=?",
|
||||
[rgid])
|
||||
if remove_missing_releases:
|
||||
for items in remove_missing_releases:
|
||||
if items['ReleaseID'] not in release_list and items['ReleaseID'] != rgid:
|
||||
@@ -435,24 +541,24 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
myDB.action("DELETE FROM tracks WHERE ReleaseID=?", [items['ReleaseID']])
|
||||
myDB.action("DELETE FROM allalbums WHERE ReleaseID=?", [items['ReleaseID']])
|
||||
myDB.action("DELETE FROM alltracks WHERE ReleaseID=?", [items['ReleaseID']])
|
||||
logger.info("Removing all references to release %s to reflect MusicBrainz" % items['ReleaseID'])
|
||||
logger.info(
|
||||
"Removing all references to release %s to reflect MusicBrainz" % items[
|
||||
'ReleaseID'])
|
||||
force_repackage1 = 1
|
||||
else:
|
||||
logger.info("There was either an error pulling data from MusicBrainz or there might not be any releases for this category")
|
||||
logger.info(
|
||||
"There was either an error pulling data from MusicBrainz or there might not be any releases for this category")
|
||||
|
||||
num_new_releases = 0
|
||||
|
||||
for releasedata in results:
|
||||
#releasedata.get will return None if it doesn't have a status
|
||||
#all official releases should have the Official status included
|
||||
if not includeExtras and releasedata.get('status') != 'Official':
|
||||
continue
|
||||
|
||||
release = {}
|
||||
rel_id_check = releasedata['id']
|
||||
album_checker = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [rel_id_check]).fetchone()
|
||||
album_checker = myDB.action('SELECT * from allalbums WHERE ReleaseID=?',
|
||||
[rel_id_check]).fetchone()
|
||||
if not album_checker or forcefull:
|
||||
#DELETE all references to this release since we're updating it anyway.
|
||||
# DELETE all references to this release since we're updating it anyway.
|
||||
myDB.action('DELETE from allalbums WHERE ReleaseID=?', [rel_id_check])
|
||||
myDB.action('DELETE from alltracks WHERE ReleaseID=?', [rel_id_check])
|
||||
release['AlbumTitle'] = unicode(releasedata['title'])
|
||||
@@ -461,7 +567,8 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None
|
||||
release['ReleaseID'] = releasedata['id']
|
||||
if 'release-group' not in releasedata:
|
||||
raise Exception('No release group associated with release id ' + releasedata['id'] + ' album id' + rgid)
|
||||
raise Exception('No release group associated with release id ' + releasedata[
|
||||
'id'] + ' album id' + rgid)
|
||||
release['Type'] = unicode(releasedata['release-group']['type'])
|
||||
|
||||
if release['Type'] == 'Album' and 'secondary-type-list' in releasedata['release-group']:
|
||||
@@ -469,7 +576,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
if secondary_type != release['Type']:
|
||||
release['Type'] = secondary_type
|
||||
|
||||
#making the assumption that the most important artist will be first in the list
|
||||
# making the assumption that the most important artist will be first in the list
|
||||
if 'artist-credit' in releasedata:
|
||||
release['ArtistID'] = unicode(releasedata['artist-credit'][0]['artist']['id'])
|
||||
release['ArtistName'] = unicode(releasedata['artist-credit-phrase'])
|
||||
@@ -477,8 +584,9 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
|
||||
return False
|
||||
|
||||
release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown'
|
||||
#assuming that the list will contain media and that the format will be consistent
|
||||
release['ReleaseCountry'] = unicode(
|
||||
releasedata['country']) if 'country' in releasedata else u'Unknown'
|
||||
# assuming that the list will contain media and that the format will be consistent
|
||||
try:
|
||||
additional_medium = ''
|
||||
for position in releasedata['medium-list']:
|
||||
@@ -490,16 +598,17 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
disc_number = ''
|
||||
else:
|
||||
disc_number = str(medium_count) + 'x'
|
||||
packaged_medium = disc_number + releasedata['medium-list'][0]['format'] + additional_medium
|
||||
packaged_medium = disc_number + releasedata['medium-list'][0][
|
||||
'format'] + additional_medium
|
||||
release['ReleaseFormat'] = unicode(packaged_medium)
|
||||
except:
|
||||
release['ReleaseFormat'] = u'Unknown'
|
||||
|
||||
release['Tracks'] = getTracksFromRelease(releasedata)
|
||||
|
||||
# What we're doing here now is first updating the allalbums & alltracks table to the most
|
||||
# current info, then moving the appropriate release into the album table and its associated
|
||||
# tracks into the tracks table
|
||||
# What we're doing here now is first updating the allalbums & alltracks table to the most
|
||||
# current info, then moving the appropriate release into the album table and its associated
|
||||
# tracks into the tracks table
|
||||
controlValueDict = {"ReleaseID": release['ReleaseID']}
|
||||
|
||||
newValueDict = {"ArtistID": release['ArtistID'],
|
||||
@@ -511,13 +620,14 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
"Type": release['Type'],
|
||||
"ReleaseCountry": release['ReleaseCountry'],
|
||||
"ReleaseFormat": release['ReleaseFormat']
|
||||
}
|
||||
}
|
||||
|
||||
myDB.upsert("allalbums", newValueDict, controlValueDict)
|
||||
|
||||
for track in release['Tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
|
||||
cleanname = helpers.clean_name(
|
||||
release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": release['ReleaseID']}
|
||||
@@ -531,30 +641,37 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
}
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?',
|
||||
[cleanname]).fetchone()
|
||||
|
||||
if not match:
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone()
|
||||
#if not match:
|
||||
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone()
|
||||
# if not match:
|
||||
# match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
if match:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location']))
|
||||
# myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?',
|
||||
(release['AlbumID'], match['Location']))
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
num_new_releases = num_new_releases + 1
|
||||
if album_checker:
|
||||
logger.info('[%s] Existing release %s (%s) updated' % (release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
logger.info('[%s] Existing release %s (%s) updated' % (
|
||||
release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
else:
|
||||
logger.info('[%s] New release %s (%s) added' % (release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
logger.info('[%s] New release %s (%s) added' % (
|
||||
release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
if force_repackage1 == 1:
|
||||
num_new_releases = -1
|
||||
logger.info('[%s] Forcing repackage of %s, since dB releases have been removed' % (release['ArtistName'], release_title))
|
||||
logger.info('[%s] Forcing repackage of %s, since dB releases have been removed' % (
|
||||
release['ArtistName'], release_title))
|
||||
else:
|
||||
num_new_releases = num_new_releases
|
||||
|
||||
@@ -571,23 +688,25 @@ def getTracksFromRelease(release):
|
||||
except:
|
||||
track_title = unicode(track['recording']['title'])
|
||||
tracks.append({
|
||||
'number': totalTracks,
|
||||
'title': track_title,
|
||||
'id': unicode(track['recording']['id']),
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['length']) if 'length' in track else 0
|
||||
})
|
||||
'number': totalTracks,
|
||||
'title': track_title,
|
||||
'id': unicode(track['recording']['id']),
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['length']) if 'length' in track else 0
|
||||
})
|
||||
totalTracks += 1
|
||||
return tracks
|
||||
|
||||
|
||||
# Used when there is a disambiguation
|
||||
|
||||
|
||||
def findArtistbyAlbum(name):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone()
|
||||
artist = myDB.action(
|
||||
'SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()',
|
||||
[name]).fetchone()
|
||||
|
||||
if not artist:
|
||||
return False
|
||||
@@ -614,40 +733,34 @@ def findArtistbyAlbum(name):
|
||||
for releaseGroup in results:
|
||||
newArtist = releaseGroup['artist-credit'][0]['artist']
|
||||
# Only need the artist ID if we're doing an artist+album lookup
|
||||
#if 'disambiguation' in newArtist:
|
||||
# if 'disambiguation' in newArtist:
|
||||
# uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")")
|
||||
#else:
|
||||
# else:
|
||||
# uniquename = unicode(newArtist['sort-name'])
|
||||
#artist_dict['name'] = unicode(newArtist['sort-name'])
|
||||
#artist_dict['uniquename'] = uniquename
|
||||
# artist_dict['name'] = unicode(newArtist['sort-name'])
|
||||
# artist_dict['uniquename'] = uniquename
|
||||
artist_dict['id'] = unicode(newArtist['id'])
|
||||
#artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
|
||||
#artist_dict['score'] = int(releaseGroup['ext:score'])
|
||||
# artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
|
||||
# artist_dict['score'] = int(releaseGroup['ext:score'])
|
||||
|
||||
return artist_dict
|
||||
|
||||
|
||||
def findAlbumID(artist=None, album=None):
|
||||
|
||||
results = None
|
||||
chars = set('!?*-')
|
||||
|
||||
try:
|
||||
if album and artist:
|
||||
if any((c in chars) for c in album):
|
||||
album = '"' + album + '"'
|
||||
if any((c in chars) for c in artist):
|
||||
artist = '"' + artist + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
criteria['artist'] = artist.lower()
|
||||
else:
|
||||
if any((c in chars) for c in album):
|
||||
album = '"' + album + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
with mb_lock:
|
||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get('release-group-list')
|
||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
|
||||
'release-group-list')
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not results:
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 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 json
|
||||
|
||||
from headphones import db, helpers, logger, request
|
||||
|
||||
|
||||
def update(artistid, artist_name, release_groups):
|
||||
""" Pretty simple and crude function to find the artist page on metacritic,
|
||||
then parse that page to get critic & user scores for albums"""
|
||||
|
||||
# First let's modify the artist name to fit the metacritic convention.
|
||||
# We could just do a search, then take the top result, but at least this will
|
||||
# cut down on api calls. If it's ineffective then we'll switch to search
|
||||
|
||||
replacements = {" & ": " ", ".": ""}
|
||||
mc_artist_name = helpers.replace_all(artist_name.lower(), replacements)
|
||||
|
||||
mc_artist_name = mc_artist_name.replace(" ", "-")
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
|
||||
url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100"
|
||||
|
||||
res = request.request_soup(url, headers=headers)
|
||||
|
||||
rows = None
|
||||
|
||||
try:
|
||||
table = res.find("table", class_="credits person_credits")
|
||||
rows = table.tbody.find_all('tr')
|
||||
except:
|
||||
logger.info("Unable to get metacritic scores for: %s" % artist_name)
|
||||
|
||||
myDB = db.DBConnection()
|
||||
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [artistid]).fetchone()
|
||||
|
||||
score_list = []
|
||||
|
||||
# If we couldn't get anything from MetaCritic for whatever reason,
|
||||
# let's try to load scores from the db
|
||||
if not rows:
|
||||
if artist['MetaCritic']:
|
||||
score_list = json.loads(artist['MetaCritic'])
|
||||
else:
|
||||
return
|
||||
|
||||
# If we did get scores, let's update the db with them
|
||||
else:
|
||||
for row in rows:
|
||||
title = row.a.string
|
||||
scores = row.find_all("span")
|
||||
critic_score = scores[0].string
|
||||
user_score = scores[1].string
|
||||
score_dict = {'title': title, 'critic_score': critic_score, 'user_score': user_score}
|
||||
score_list.append(score_dict)
|
||||
|
||||
# Save scores to the database
|
||||
controlValueDict = {"ArtistID": artistid}
|
||||
newValueDict = {'MetaCritic': json.dumps(score_list)}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
for score in score_list:
|
||||
title = score['title']
|
||||
# Iterate through the release groups we got passed to see if we can find
|
||||
# a match
|
||||
for rg in release_groups:
|
||||
if rg['title'].lower() == title.lower():
|
||||
critic_score = score['critic_score']
|
||||
user_score = score['user_score']
|
||||
controlValueDict = {"AlbumID": rg['id']}
|
||||
newValueDict = {'CriticScore': critic_score, 'UserScore': user_score}
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
@@ -0,0 +1,376 @@
|
||||
# encoding=utf8
|
||||
# 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/>.
|
||||
"""
|
||||
Track/album metadata handling routines.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
from beets.mediafile import MediaFile, UnreadableFileError
|
||||
import headphones
|
||||
from headphones import logger
|
||||
import os.path
|
||||
import datetime
|
||||
|
||||
__author__ = "Andrzej Ciarkowski <andrzej.ciarkowski@gmail.com>"
|
||||
|
||||
|
||||
class MetadataDict(dict):
|
||||
"""
|
||||
Dictionary which allows for case-insensitive, but case-preserving lookup,
|
||||
allowing to put different values under $Album and $album, but still
|
||||
finding some value if only single key is present and called with any
|
||||
variation of the name's case.
|
||||
|
||||
Keeps case-sensitive mapping in superclass dict, and case-insensitive (
|
||||
lowercase) in member variable self._lower. If case-sensitive lookup
|
||||
fails, another case-insensitive attempt is made.
|
||||
"""
|
||||
def __setitem__(self, key, value):
|
||||
super(MetadataDict, self).__setitem__(key, value)
|
||||
self._lower.__setitem__(key.lower(), value)
|
||||
|
||||
def add_items(self, items):
|
||||
# type: (Iterable[Tuple[Any,Any]])->None
|
||||
"""
|
||||
Add (key,value) pairs to this dictionary using iterable as an input.
|
||||
:param items: input items.
|
||||
"""
|
||||
for key, value in items:
|
||||
self.__setitem__(key, value)
|
||||
|
||||
def __init__(self, seq=None, **kwargs):
|
||||
if isinstance(seq, MetadataDict):
|
||||
super(MetadataDict, self).__init__(seq)
|
||||
self._lower = dict(seq._lower)
|
||||
else:
|
||||
super(MetadataDict, self).__init__()
|
||||
self._lower = {}
|
||||
if seq is not None:
|
||||
try:
|
||||
self.add_items(seq.iteritems())
|
||||
except KeyError:
|
||||
self.add_items(seq)
|
||||
|
||||
def __getitem__(self, item):
|
||||
try:
|
||||
return super(MetadataDict, self).__getitem__(item)
|
||||
except KeyError:
|
||||
return self._lower.__getitem__(item.lower())
|
||||
|
||||
def __contains__(self, item):
|
||||
return self._lower.__contains__(item.lower())
|
||||
|
||||
|
||||
class Vars:
|
||||
"""
|
||||
Metadata $variable names (only ones set explicitly by headphones).
|
||||
"""
|
||||
DISC = '$Disc'
|
||||
TRACK = '$Track'
|
||||
TITLE = '$Title'
|
||||
ARTIST = '$Artist'
|
||||
SORT_ARTIST = '$SortArtist'
|
||||
ALBUM = '$Album'
|
||||
YEAR = '$Year'
|
||||
DATE = '$Date'
|
||||
EXTENSION = '$Extension'
|
||||
ORIGINAL_FOLDER = '$OriginalFolder'
|
||||
FIRST_LETTER = '$First'
|
||||
TYPE = '$Type'
|
||||
TITLE_LOWER = TITLE.lower()
|
||||
ARTIST_LOWER = ARTIST.lower()
|
||||
SORT_ARTIST_LOWER = SORT_ARTIST.lower()
|
||||
ALBUM_LOWER = ALBUM.lower()
|
||||
ORIGINAL_FOLDER_LOWER = ORIGINAL_FOLDER.lower()
|
||||
FIRST_LETTER_LOWER = FIRST_LETTER.lower()
|
||||
TYPE_LOWER = TYPE.lower()
|
||||
|
||||
|
||||
def _verify_var_type(val):
|
||||
"""
|
||||
Check if type of value is allowed as a variable in pathname substitution.
|
||||
"""
|
||||
return isinstance(val, (basestring, int, float, datetime.date))
|
||||
|
||||
|
||||
def _as_str(val):
|
||||
if isinstance(val, basestring):
|
||||
return val
|
||||
else:
|
||||
return str(val)
|
||||
|
||||
|
||||
def _media_file_to_dict(mf, d):
|
||||
# type: (MediaFile, MutableMapping[basestring,basestring])->None
|
||||
"""
|
||||
Populate dict with tags read from media file.
|
||||
"""
|
||||
for fld in mf.readable_fields():
|
||||
if 'art' == fld:
|
||||
# skip embedded artwork as it's a BLOB
|
||||
continue
|
||||
val = getattr(mf, fld)
|
||||
if val is None:
|
||||
val = ''
|
||||
# include only types with meaningful string representation
|
||||
if _verify_var_type(val):
|
||||
d['$' + fld] = _as_str(val)
|
||||
|
||||
|
||||
def _row_to_dict(row, d):
|
||||
"""
|
||||
Populate dict with database row fields.
|
||||
"""
|
||||
for fld in row.keys():
|
||||
val = row[fld]
|
||||
if val is None:
|
||||
val = ''
|
||||
if _verify_var_type(val):
|
||||
d['$' + fld] = _as_str(val)
|
||||
|
||||
|
||||
def _date_year(release):
|
||||
# type: (sqlite3.Row)->Tuple[str,str]
|
||||
"""
|
||||
Extract release date and year from database row
|
||||
"""
|
||||
try:
|
||||
date = release['ReleaseDate']
|
||||
except TypeError:
|
||||
date = ''
|
||||
|
||||
if date is not None:
|
||||
year = date[:4]
|
||||
else:
|
||||
year = ''
|
||||
return date, year
|
||||
|
||||
|
||||
def _lower(s):
|
||||
# type: basestring->basestring
|
||||
"""
|
||||
Return s.lower() if not None
|
||||
:param s:
|
||||
:return:
|
||||
"""
|
||||
if s:
|
||||
return s.lower()
|
||||
return None
|
||||
|
||||
|
||||
def file_metadata(path, release):
|
||||
# type: (str,sqlite3.Row)->Tuple[Mapping[str,str],bool]
|
||||
"""
|
||||
Prepare metadata dictionary for path substitution, based on file name,
|
||||
the tags stored within it and release info from the db.
|
||||
:param path: media file path
|
||||
:param release: database row with release info
|
||||
:return: pair (dict,boolean indicating if Vars.TITLE is taken from tags or
|
||||
file name). (None,None) if unable to parse the media file.
|
||||
"""
|
||||
try:
|
||||
f = MediaFile(path)
|
||||
except UnreadableFileError as ex:
|
||||
logger.info("MediaFile couldn't parse: %s (%s)",
|
||||
path.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
str(ex))
|
||||
return None, None
|
||||
|
||||
res = MetadataDict()
|
||||
# add existing tags first, these will get overwritten by musicbrainz from db
|
||||
_media_file_to_dict(f, res)
|
||||
# raw database fields come next
|
||||
_row_to_dict(release, res)
|
||||
|
||||
date, year = _date_year(release)
|
||||
if not f.disc:
|
||||
disc_number = ''
|
||||
else:
|
||||
disc_number = '%d' % f.disc
|
||||
|
||||
if not f.track:
|
||||
track_number = ''
|
||||
else:
|
||||
track_number = '%02d' % f.track
|
||||
|
||||
if not f.title:
|
||||
basename = os.path.basename(
|
||||
path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
title = os.path.splitext(basename)[0]
|
||||
from_metadata = False
|
||||
else:
|
||||
title = f.title
|
||||
from_metadata = True
|
||||
|
||||
ext = os.path.splitext(path)[1]
|
||||
if release['ArtistName'] == "Various Artists" and f.artist:
|
||||
artist_name = f.artist
|
||||
else:
|
||||
artist_name = release['ArtistName']
|
||||
|
||||
if artist_name and artist_name.startswith('The '):
|
||||
sort_name = artist_name[4:] + ", The"
|
||||
else:
|
||||
sort_name = artist_name
|
||||
|
||||
album_title = release['AlbumTitle']
|
||||
override_values = {
|
||||
Vars.DISC: disc_number,
|
||||
Vars.TRACK: track_number,
|
||||
Vars.TITLE: title,
|
||||
Vars.ARTIST: artist_name,
|
||||
Vars.SORT_ARTIST: sort_name,
|
||||
Vars.ALBUM: album_title,
|
||||
Vars.YEAR: year,
|
||||
Vars.DATE: date,
|
||||
Vars.EXTENSION: ext,
|
||||
Vars.TITLE_LOWER: _lower(title),
|
||||
Vars.ARTIST_LOWER: _lower(artist_name),
|
||||
Vars.SORT_ARTIST_LOWER: _lower(sort_name),
|
||||
Vars.ALBUM_LOWER: _lower(album_title),
|
||||
}
|
||||
res.add_items(override_values.iteritems())
|
||||
return res, from_metadata
|
||||
|
||||
|
||||
def _intersect(d1, d2):
|
||||
# type: (Mapping,Mapping)->Mapping
|
||||
"""
|
||||
Create intersection (common part) of two dictionaries.
|
||||
"""
|
||||
res = {}
|
||||
for key, val in d1.iteritems():
|
||||
if key in d2 and d2[key] == val:
|
||||
res[key] = val
|
||||
return res
|
||||
|
||||
|
||||
def album_metadata(path, release, common_tags):
|
||||
# type: (str,sqlite3.Row,Mapping[str,str])->Mapping[str,str]
|
||||
"""
|
||||
Prepare metadata dictionary for path substitution of album folder.
|
||||
:param path: album path to prepare metadata for.
|
||||
:param release: database row with release properties.
|
||||
:param common_tags: common set of tags gathered from media files.
|
||||
:return: metadata dictionary with substitution variables for rendering path.
|
||||
"""
|
||||
date, year = _date_year(release)
|
||||
artist = release['ArtistName']
|
||||
if artist:
|
||||
artist = artist.replace('/', '_')
|
||||
album = release['AlbumTitle']
|
||||
if album:
|
||||
album = album.replace('/', '_')
|
||||
release_type = release['Type']
|
||||
if release_type:
|
||||
release_type = release_type.replace('/', '_')
|
||||
|
||||
if artist and artist.startswith('The '):
|
||||
sort_name = artist[4:] + ", The"
|
||||
else:
|
||||
sort_name = artist
|
||||
|
||||
if not sort_name or sort_name[0].isdigit():
|
||||
first_char = u'0-9'
|
||||
else:
|
||||
first_char = sort_name[0]
|
||||
|
||||
orig_folder = u''
|
||||
for r, d, f in os.walk(path):
|
||||
try:
|
||||
orig_folder = os.path.basename(
|
||||
os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
override_values = {
|
||||
Vars.ARTIST: artist,
|
||||
Vars.SORT_ARTIST: sort_name,
|
||||
Vars.ALBUM: album,
|
||||
Vars.YEAR: year,
|
||||
Vars.DATE: date,
|
||||
Vars.TYPE: release_type,
|
||||
Vars.ORIGINAL_FOLDER: orig_folder,
|
||||
Vars.FIRST_LETTER: first_char.upper(),
|
||||
Vars.ARTIST_LOWER: _lower(artist),
|
||||
Vars.SORT_ARTIST_LOWER: _lower(sort_name),
|
||||
Vars.ALBUM_LOWER: _lower(album),
|
||||
Vars.TYPE_LOWER: _lower(release_type),
|
||||
Vars.FIRST_LETTER_LOWER: _lower(first_char),
|
||||
Vars.ORIGINAL_FOLDER_LOWER: _lower(orig_folder)
|
||||
}
|
||||
res = MetadataDict(common_tags)
|
||||
res.add_items(override_values.iteritems())
|
||||
return res
|
||||
|
||||
|
||||
def albumart_metadata(release, common_tags):
|
||||
# type: (sqlite3.Row,Mapping)->Mapping
|
||||
"""
|
||||
Prepare metadata dictionary for path subtitution of album art file.
|
||||
:param release: database row with release properties.
|
||||
:param common_tags: common set of tags gathered from media files.
|
||||
:return: metadata dictionary with substitution variables for rendering path.
|
||||
"""
|
||||
date, year = _date_year(release)
|
||||
artist = release['ArtistName']
|
||||
album = release['AlbumTitle']
|
||||
|
||||
override_values = {
|
||||
Vars.ARTIST: artist,
|
||||
Vars.ALBUM: album,
|
||||
Vars.YEAR: year,
|
||||
Vars.DATE: date,
|
||||
Vars.ARTIST_LOWER: _lower(artist),
|
||||
Vars.ALBUM_LOWER: _lower(album)
|
||||
}
|
||||
res = MetadataDict(common_tags)
|
||||
res.add_items(override_values.iteritems())
|
||||
return res
|
||||
|
||||
|
||||
class AlbumMetadataBuilder(object):
|
||||
"""
|
||||
Facilitates building of album metadata as a common set of tags retrieved
|
||||
from media files.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._common = None
|
||||
|
||||
def add_media_file(self, mf):
|
||||
# type: (Mapping)->None
|
||||
"""
|
||||
Add metadata tags read from media file to album metadata.
|
||||
:param mf: MediaFile
|
||||
"""
|
||||
md = {}
|
||||
_media_file_to_dict(mf, md)
|
||||
if self._common is None:
|
||||
self._common = md
|
||||
else:
|
||||
self._common = _intersect(self._common, md)
|
||||
|
||||
def build(self):
|
||||
# type: (None)->Mapping
|
||||
"""
|
||||
Build case-insensitive, case-preserving dict from gathered metadata
|
||||
tags.
|
||||
:return: dictinary-like object filled with $variables based on common
|
||||
tags.
|
||||
"""
|
||||
return MetadataDict(self._common)
|
||||
@@ -0,0 +1,175 @@
|
||||
# encoding=utf8
|
||||
# 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/>.
|
||||
"""
|
||||
Test module for metadata.
|
||||
"""
|
||||
import headphones as _h
|
||||
import headphones.metadata as _md
|
||||
import headphones.helpers as _hp
|
||||
from headphones.metadata import MetadataDict
|
||||
import datetime
|
||||
|
||||
from unittestcompat import TestCase
|
||||
|
||||
|
||||
__author__ = "Andrzej Ciarkowski <andrzej.ciarkowski@gmail.com>"
|
||||
|
||||
|
||||
class _MockMediaFile(object):
|
||||
|
||||
def __init__(self, artist, album, year, track, title, label):
|
||||
self.artist = artist
|
||||
self.album = album
|
||||
self.year = year
|
||||
self.track = track
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.art = 'THIS IS ART BLOB'
|
||||
|
||||
@classmethod
|
||||
def readable_fields(cls):
|
||||
return 'artist', 'album', 'year', 'track', 'title', 'label', 'art'
|
||||
|
||||
|
||||
class _MockDatabaseRow(object):
|
||||
|
||||
def __init__(self, d):
|
||||
self._dict = dict(d)
|
||||
|
||||
def keys(self):
|
||||
return self._dict.iterkeys()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._dict[item]
|
||||
|
||||
|
||||
class MetadataTest(TestCase):
|
||||
"""
|
||||
Tests for metadata module.
|
||||
"""
|
||||
|
||||
def test_metadata_dict_ci(self):
|
||||
"""MetadataDict: case-insensitive lookup"""
|
||||
expected = u'naïve'
|
||||
key_var = '$TitlE'
|
||||
m = MetadataDict({key_var.lower(): u'naïve'})
|
||||
self.assertFalse('$track' in m)
|
||||
self.assertTrue('$tITLe' in m, "cross-case lookup with 'in'")
|
||||
self.assertEqual(m[key_var], expected, "cross-case lookup success")
|
||||
self.assertEqual(m[key_var.lower()], expected, "same-case lookup "
|
||||
"succes")
|
||||
|
||||
def test_metadata_dict_cs(self):
|
||||
"""MetadataDice: case-preserving lookup"""
|
||||
expected_var = u'NaïVe'
|
||||
key_var = '$TitlE'
|
||||
m = MetadataDict({
|
||||
key_var.lower(): expected_var.lower(),
|
||||
key_var: expected_var
|
||||
})
|
||||
self.assertFalse('$track' in m)
|
||||
self.assertTrue('$tITLe' in m, "cross-case lookup with 'in'")
|
||||
self.assertEqual(m[key_var.lower()], expected_var.lower(),
|
||||
"case-preserving lookup lower")
|
||||
self.assertEqual(m[key_var], expected_var,
|
||||
"case-preserving lookup variable")
|
||||
|
||||
def test_dict_intersect(self):
|
||||
"""metadata: check dictionary intersect function validity"""
|
||||
d1 = {
|
||||
'one': 'one',
|
||||
'two': 'two',
|
||||
'three': 'zonk'
|
||||
}
|
||||
d2 = {
|
||||
'two': 'two',
|
||||
'three': 'three'
|
||||
}
|
||||
expected = {
|
||||
'two': 'two'
|
||||
}
|
||||
self.assertItemsEqual(
|
||||
expected, _md._intersect(d1, d2), "check dictionary intersection "
|
||||
"is common part indeed"
|
||||
)
|
||||
del d1['two']
|
||||
expected = {}
|
||||
self.assertItemsEqual(
|
||||
expected, _md._intersect(d1, d2), "check intersection empty"
|
||||
)
|
||||
|
||||
def test_album_metadata_builder(self):
|
||||
"""AlbumMetadataBuilder: check validity"""
|
||||
mb = _md.AlbumMetadataBuilder()
|
||||
f1 = _MockMediaFile('artist', 'album', 2000, 1, 'track1', 'Ant-Zen')
|
||||
mb.add_media_file(f1)
|
||||
f2 = _MockMediaFile('artist', 'album', 2000, 2, 'track2', 'Ant-Zen')
|
||||
mb.add_media_file(f2)
|
||||
|
||||
md = mb.build()
|
||||
expected = {
|
||||
_md.Vars.ARTIST_LOWER: 'artist',
|
||||
_md.Vars.ALBUM_LOWER: 'album',
|
||||
_md.Vars.YEAR.lower(): 2000,
|
||||
'$label': 'Ant-Zen'
|
||||
}
|
||||
self.assertItemsEqual(
|
||||
expected, md, "check AlbumMetadataBuilder validity"
|
||||
)
|
||||
|
||||
def test_populate_from_row(self):
|
||||
"""metadata: check populating metadata from database row"""
|
||||
row = _MockDatabaseRow({
|
||||
'ArtistName': 'artist',
|
||||
'AlbumTitle': 'album',
|
||||
'ReleaseDate': datetime.date(2004, 11, 28),
|
||||
'Variation': 5,
|
||||
'WrongTyped': complex(1, -1)
|
||||
})
|
||||
md = _md.MetadataDict()
|
||||
_md._row_to_dict(row, md)
|
||||
expected = {
|
||||
'$ArtistName': 'artist',
|
||||
'$AlbumTitle': 'album',
|
||||
'$ReleaseDate': '2004-11-28',
|
||||
'$Variation': '5'
|
||||
}
|
||||
self.assertItemsEqual(expected, md, "check _row_to_dict() valid")
|
||||
|
||||
def test_album_metadata_with_None(self):
|
||||
"""metadata: check handling of None metadata values"""
|
||||
row = _MockDatabaseRow({
|
||||
'ArtistName': 'artist',
|
||||
'AlbumTitle': 'Album',
|
||||
'Type': None,
|
||||
'ReleaseDate': None,
|
||||
})
|
||||
mb = _md.AlbumMetadataBuilder()
|
||||
f1 = _MockMediaFile('artist', None, None, None, None, None)
|
||||
mb.add_media_file(f1)
|
||||
f2 = _MockMediaFile('artist', None, None, 2, 'track2', None)
|
||||
mb.add_media_file(f2)
|
||||
md = _md.album_metadata("/music/Artist - Album [2002]", row, mb.build())
|
||||
|
||||
# tests don't undergo normal Headphones init, SYS_ENCODING is not set
|
||||
if not _h.SYS_ENCODING:
|
||||
_h.SYS_ENCODING = 'UTF-8'
|
||||
|
||||
res = _hp.replace_all(
|
||||
"/music/$First/$Artist/$Artist - $Album{ [$Year]}", md, True)
|
||||
|
||||
self.assertEqual(res, u"/music/A/artist/artist - Album",
|
||||
"check correct rendering of None via replace_all()")
|
||||
+44
-24
@@ -13,16 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import subprocess
|
||||
import headphones
|
||||
import multiprocessing
|
||||
|
||||
import os
|
||||
import headphones
|
||||
from headphones import logger
|
||||
from beets.mediafile import MediaFile
|
||||
|
||||
|
||||
# xld
|
||||
import getXldProfile
|
||||
|
||||
@@ -32,9 +33,11 @@ def encode(albumPath):
|
||||
|
||||
# Return if xld details not found
|
||||
if use_xld:
|
||||
(xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
|
||||
(xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(
|
||||
headphones.CONFIG.XLDPROFILE)
|
||||
if not xldFormat:
|
||||
logger.error('Details for xld profile \'%s\' not found, files will not be re-encoded', xldProfile)
|
||||
logger.error('Details for xld profile \'%s\' not found, files will not be re-encoded',
|
||||
xldProfile)
|
||||
return None
|
||||
else:
|
||||
xldProfile = None
|
||||
@@ -60,7 +63,8 @@ def encode(albumPath):
|
||||
for music in f:
|
||||
if any(music.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
if not use_xld:
|
||||
encoderFormat = headphones.CONFIG.ENCODEROUTPUTFORMAT.encode(headphones.SYS_ENCODING)
|
||||
encoderFormat = headphones.CONFIG.ENCODEROUTPUTFORMAT.encode(
|
||||
headphones.SYS_ENCODING)
|
||||
else:
|
||||
xldMusicFile = os.path.join(r, music)
|
||||
xldInfoMusic = MediaFile(xldMusicFile)
|
||||
@@ -68,9 +72,11 @@ def encode(albumPath):
|
||||
|
||||
if headphones.CONFIG.ENCODERLOSSLESS:
|
||||
ext = os.path.normpath(os.path.splitext(music)[1].lstrip(".")).lower()
|
||||
if not use_xld and ext == 'flac' or use_xld and (ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 400)):
|
||||
if not use_xld and ext == 'flac' or use_xld and (
|
||||
ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 400)):
|
||||
musicFiles.append(os.path.join(r, music))
|
||||
musicTemp = os.path.normpath(os.path.splitext(music)[0] + '.' + encoderFormat)
|
||||
musicTemp = os.path.normpath(
|
||||
os.path.splitext(music)[0] + '.' + encoderFormat)
|
||||
musicTempFiles.append(os.path.join(tempDirEncode, musicTemp))
|
||||
else:
|
||||
logger.debug('%s is already encoded', music)
|
||||
@@ -86,7 +92,7 @@ def encode(albumPath):
|
||||
encoder = os.path.join('/Applications', 'xld')
|
||||
elif headphones.CONFIG.ENCODER == 'lame':
|
||||
if headphones.SYS_PLATFORM == "win32":
|
||||
## NEED THE DEFAULT LAME INSTALL ON WIN!
|
||||
# NEED THE DEFAULT LAME INSTALL ON WIN!
|
||||
encoder = "C:/Program Files/lame/lame.exe"
|
||||
else:
|
||||
encoder = "lame"
|
||||
@@ -111,24 +117,31 @@ def encode(albumPath):
|
||||
|
||||
if use_xld:
|
||||
if xldBitrate and (infoMusic.bitrate / 1000 <= xldBitrate):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music.decode(headphones.SYS_ENCODING, 'replace'), xldBitrate)
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded',
|
||||
music.decode(headphones.SYS_ENCODING, 'replace'), xldBitrate)
|
||||
else:
|
||||
encode = True
|
||||
elif headphones.CONFIG.ENCODER == 'lame':
|
||||
if not any(music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x in ["mp3", "wav"]):
|
||||
logger.warn('Lame cannot encode %s format for %s, use ffmpeg', os.path.splitext(music)[1], music)
|
||||
if not any(
|
||||
music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x
|
||||
in ["mp3", "wav"]):
|
||||
logger.warn('Lame cannot encode %s format for %s, use ffmpeg',
|
||||
os.path.splitext(music)[1], music)
|
||||
else:
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music, headphones.CONFIG.BITRATE)
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (
|
||||
int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music,
|
||||
headphones.CONFIG.BITRATE)
|
||||
else:
|
||||
encode = True
|
||||
else:
|
||||
if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'ogg':
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.ogg'):
|
||||
logger.warn('Cannot re-encode .ogg %s', music.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn('Cannot re-encode .ogg %s',
|
||||
music.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
else:
|
||||
encode = True
|
||||
elif headphones.CONFIG.ENCODEROUTPUTFORMAT == 'mp3' or headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a':
|
||||
else:
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + headphones.CONFIG.ENCODEROUTPUTFORMAT) and (int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music, headphones.CONFIG.BITRATE)
|
||||
else:
|
||||
@@ -155,7 +168,7 @@ def encode(albumPath):
|
||||
processes = headphones.CONFIG.ENCODER_MULTICORE_COUNT
|
||||
|
||||
logger.debug("Multi-core encoding enabled, spawning %d processes",
|
||||
processes)
|
||||
processes)
|
||||
|
||||
# Use multiprocessing only if it's worth the overhead. and if it is
|
||||
# enabled. If not, then use the old fashioned way.
|
||||
@@ -185,7 +198,8 @@ def encode(albumPath):
|
||||
for dest in musicTempFiles:
|
||||
if not os.path.exists(dest):
|
||||
encoder_failed = True
|
||||
logger.error("Encoded file '%s' does not exist in the destination temp directory", dest)
|
||||
logger.error("Encoded file '%s' does not exist in the destination temp directory",
|
||||
dest)
|
||||
|
||||
# No errors, move from temp to parent
|
||||
if not encoder_failed and musicTempFiles:
|
||||
@@ -211,7 +225,9 @@ def encode(albumPath):
|
||||
|
||||
# Return with error if any encoding errors
|
||||
if encoder_failed:
|
||||
logger.error("One or more files failed to encode. Ensure you have the latest version of %s installed.", headphones.CONFIG.ENCODER)
|
||||
logger.error(
|
||||
"One or more files failed to encode. Ensure you have the latest version of %s installed.",
|
||||
headphones.CONFIG.ENCODER)
|
||||
return None
|
||||
|
||||
time.sleep(1)
|
||||
@@ -269,7 +285,8 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
if not headphones.CONFIG.ADVANCEDENCODER:
|
||||
opts.extend(['-h'])
|
||||
if headphones.CONFIG.ENCODERVBRCBR == 'cbr':
|
||||
opts.extend(['--resample', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-b', str(headphones.CONFIG.BITRATE)])
|
||||
opts.extend(['--resample', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-b',
|
||||
str(headphones.CONFIG.BITRATE)])
|
||||
elif headphones.CONFIG.ENCODERVBRCBR == 'vbr':
|
||||
opts.extend(['-v', str(headphones.CONFIG.ENCODERQUALITY)])
|
||||
else:
|
||||
@@ -290,7 +307,8 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a':
|
||||
opts.extend(['-strict', 'experimental'])
|
||||
if headphones.CONFIG.ENCODERVBRCBR == 'cbr':
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab', str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab',
|
||||
str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
elif headphones.CONFIG.ENCODERVBRCBR == 'vbr':
|
||||
opts.extend(['-aq', str(headphones.CONFIG.ENCODERQUALITY)])
|
||||
opts.extend(['-y', '-ac', '2', '-vn'])
|
||||
@@ -311,7 +329,8 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a':
|
||||
opts.extend(['-strict', 'experimental'])
|
||||
if headphones.CONFIG.ENCODERVBRCBR == 'cbr':
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab', str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab',
|
||||
str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
elif headphones.CONFIG.ENCODERVBRCBR == 'vbr':
|
||||
opts.extend(['-aq', str(headphones.CONFIG.ENCODERQUALITY)])
|
||||
opts.extend(['-y', '-ac', '2', '-vn'])
|
||||
@@ -337,13 +356,14 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
logger.debug(subprocess.list2cmdline(cmd))
|
||||
|
||||
process = subprocess.Popen(cmd, startupinfo=startupinfo,
|
||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = process.communicate(headphones.CONFIG.ENCODER)
|
||||
|
||||
# Error if return code not zero
|
||||
if process.returncode:
|
||||
logger.error('Encoding failed for %s' % (musicSource.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
logger.error(
|
||||
'Encoding failed for %s' % (musicSource.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
out = stdout if stdout else stderr
|
||||
out = out.decode(headphones.SYS_ENCODING, 'replace')
|
||||
outlast2lines = '\n'.join(out.splitlines()[-2:])
|
||||
|
||||
+225
-164
@@ -13,27 +13,27 @@
|
||||
# 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 logger, helpers, common, request
|
||||
|
||||
from xml.dom import minidom
|
||||
from urllib import urlencode
|
||||
import urllib
|
||||
import subprocess
|
||||
import json
|
||||
from email.mime.text import MIMEText
|
||||
import smtplib
|
||||
import email.utils
|
||||
from httplib import HTTPSConnection
|
||||
from urlparse import parse_qsl
|
||||
from urllib import urlencode
|
||||
from pynma import pynma
|
||||
|
||||
import base64
|
||||
import cherrypy
|
||||
import urllib
|
||||
import urllib2
|
||||
import headphones
|
||||
import os.path
|
||||
import subprocess
|
||||
import gntp.notifier
|
||||
import json
|
||||
|
||||
import os.path
|
||||
from headphones import logger, helpers, common, request
|
||||
from pynma import pynma
|
||||
import cherrypy
|
||||
import headphones
|
||||
import gntp.notifier
|
||||
import oauth2 as oauth
|
||||
import pythontwitter as twitter
|
||||
|
||||
|
||||
class GROWL(object):
|
||||
"""
|
||||
Growl notifications, for OS X.
|
||||
@@ -54,6 +54,7 @@ class GROWL(object):
|
||||
# Split host and port
|
||||
if self.host == "":
|
||||
host, port = "localhost", 23053
|
||||
|
||||
if ":" in self.host:
|
||||
host, port = self.host.split(':', 1)
|
||||
port = int(port)
|
||||
@@ -90,7 +91,7 @@ class GROWL(object):
|
||||
|
||||
# Send it, including an image
|
||||
image_file = os.path.join(str(headphones.PROG_DIR),
|
||||
"data/images/headphoneslogo.png")
|
||||
"data/images/headphoneslogo.png")
|
||||
|
||||
with open(image_file, 'rb') as f:
|
||||
image = f.read()
|
||||
@@ -109,7 +110,7 @@ class GROWL(object):
|
||||
logger.info(u"Growl notifications sent.")
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, host, password):
|
||||
@@ -146,24 +147,24 @@ class PROWL(object):
|
||||
'priority': headphones.CONFIG.PROWL_PRIORITY}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Prowl notifications sent.")
|
||||
return True
|
||||
logger.info(u"Prowl notifications sent.")
|
||||
return True
|
||||
elif request_status == 401:
|
||||
logger.info(u"Prowl auth failed: %s" % response.reason)
|
||||
return False
|
||||
logger.info(u"Prowl auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Prowl notification failed.")
|
||||
return False
|
||||
logger.info(u"Prowl notification failed.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
@@ -180,7 +181,6 @@ class MPC(object):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
|
||||
def notify(self):
|
||||
@@ -213,9 +213,11 @@ class XBMC(object):
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
@@ -239,20 +241,23 @@ class XBMC(object):
|
||||
|
||||
header = "Headphones"
|
||||
message = "%s - %s added to your library" % (artist, album)
|
||||
time = "3000" # in ms
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to XMBC @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']
|
||||
version = self._sendjson(host, 'Application.GetProperties',
|
||||
{'properties': ['version']})['version']['major']
|
||||
|
||||
if version < 12: #Eden
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: #Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time), 'image': albumartpath}
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
|
||||
if not request:
|
||||
@@ -311,34 +316,34 @@ class Plex(object):
|
||||
self.client_hosts = headphones.CONFIG.PLEX_CLIENT_HOST
|
||||
self.username = headphones.CONFIG.PLEX_USERNAME
|
||||
self.password = headphones.CONFIG.PLEX_PASSWORD
|
||||
self.token = headphones.CONFIG.PLEX_TOKEN
|
||||
|
||||
def _sendhttp(self, host, command):
|
||||
|
||||
username = self.username
|
||||
password = self.password
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + command
|
||||
|
||||
url_command = urllib.urlencode(command)
|
||||
|
||||
url = host + '/xbmcCmds/xbmcHttp/?' + url_command
|
||||
|
||||
req = urllib2.Request(url)
|
||||
|
||||
if password:
|
||||
base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
|
||||
req.add_header("Authorization", "Basic %s" % base64string)
|
||||
|
||||
logger.info('Plex url: %s' % url)
|
||||
|
||||
try:
|
||||
handle = urllib2.urlopen(req)
|
||||
except Exception as e:
|
||||
logger.warn('Error opening Plex url: %s' % e)
|
||||
return
|
||||
|
||||
response = handle.read().decode(headphones.SYS_ENCODING)
|
||||
if self.password:
|
||||
response = request.request_response(url, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_response(url)
|
||||
|
||||
return response
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
|
||||
def update(self):
|
||||
|
||||
# From what I read you can't update the music library on a per directory or per path basis
|
||||
@@ -349,13 +354,15 @@ class Plex(object):
|
||||
for host in hosts:
|
||||
logger.info('Sending library update command to Plex Media Server@ ' + host)
|
||||
url = "%s/library/sections" % host
|
||||
try:
|
||||
xml_sections = minidom.parse(urllib.urlopen(url))
|
||||
except IOError, e:
|
||||
logger.warn("Error while trying to contact Plex Media Server: %s" % e)
|
||||
return False
|
||||
if self.token:
|
||||
params = {'X-Plex-Token': self.token}
|
||||
else:
|
||||
params = False
|
||||
|
||||
r = request.request_minidom(url, params=params)
|
||||
|
||||
sections = r.getElementsByTagName('Directory')
|
||||
|
||||
sections = xml_sections.getElementsByTagName('Directory')
|
||||
if not sections:
|
||||
logger.info(u"Plex Media Server not running on: " + host)
|
||||
return False
|
||||
@@ -363,11 +370,7 @@ class Plex(object):
|
||||
for s in sections:
|
||||
if s.getAttribute('type') == "artist":
|
||||
url = "%s/library/sections/%s/refresh" % (host, s.getAttribute('key'))
|
||||
try:
|
||||
urllib.urlopen(url)
|
||||
except Exception as e:
|
||||
logger.warn("Error updating library section for Plex Media Server: %s" % e)
|
||||
return False
|
||||
request.request_response(url, params=params)
|
||||
|
||||
def notify(self, artist, album, albumartpath):
|
||||
|
||||
@@ -375,20 +378,30 @@ class Plex(object):
|
||||
|
||||
header = "Headphones"
|
||||
message = "%s - %s added to your library" % (artist, album)
|
||||
time = "3000" # in ms
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to Plex Media Server @ ' + host)
|
||||
logger.info('Sending notification command to Plex client @ ' + host)
|
||||
try:
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
version = self._sendjson(host, 'Application.GetProperties',
|
||||
{'properties': ['version']})['version']['major']
|
||||
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
|
||||
if not request:
|
||||
raise Exception
|
||||
|
||||
except:
|
||||
logger.warn('Error sending notification request to Plex Media Server')
|
||||
except Exception:
|
||||
logger.error('Error sending notification request to Plex client @ ' + host)
|
||||
|
||||
|
||||
class NMA(object):
|
||||
@@ -430,60 +443,37 @@ class NMA(object):
|
||||
|
||||
|
||||
class PUSHBULLET(object):
|
||||
|
||||
def __init__(self):
|
||||
self.apikey = headphones.CONFIG.PUSHBULLET_APIKEY
|
||||
self.deviceid = headphones.CONFIG.PUSHBULLET_DEVICEID
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('PUSHBULLET', options)
|
||||
|
||||
def notify(self, message, event):
|
||||
def notify(self, message, status):
|
||||
if not headphones.CONFIG.PUSHBULLET_ENABLED:
|
||||
return
|
||||
|
||||
http_handler = HTTPSConnection("api.pushbullet.com")
|
||||
url = "https://api.pushbullet.com/v2/pushes"
|
||||
|
||||
data = {'type': "note",
|
||||
'title': "Headphones",
|
||||
'body': message.encode("utf-8")}
|
||||
'body': message + ': ' + status}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/v2/pushes",
|
||||
headers={'Content-type': "application/json",
|
||||
'Authorization': 'Basic %s' % base64.b64encode(headphones.CONFIG.PUSHBULLET_APIKEY + ":")},
|
||||
body=json.dumps(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
logger.debug(u"PushBullet response status: %r" % request_status)
|
||||
logger.debug(u"PushBullet response headers: %r" % response.getheaders())
|
||||
logger.debug(u"PushBullet response body: %r" % response.read())
|
||||
if self.deviceid:
|
||||
data['device_iden'] = self.deviceid
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"PushBullet notifications sent.")
|
||||
return True
|
||||
elif request_status >= 400 and request_status < 500:
|
||||
logger.info(u"PushBullet request failed: %s" % response.reason)
|
||||
return False
|
||||
headers = {'Content-type': "application/json",
|
||||
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
|
||||
response = request.request_json(url, method="post", headers=headers, data=json.dumps(data))
|
||||
|
||||
if response:
|
||||
logger.info(u"PushBullet notifications sent.")
|
||||
return True
|
||||
else:
|
||||
logger.info(u"PushBullet notification failed serverside.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, apikey, deviceid):
|
||||
|
||||
self.enabled = True
|
||||
self.apikey = apikey
|
||||
self.deviceid = deviceid
|
||||
|
||||
self.notify('Main Screen Activate', 'Test Message')
|
||||
logger.info(u"PushBullet notification failed.")
|
||||
return False
|
||||
|
||||
|
||||
class PUSHALOT(object):
|
||||
|
||||
def notify(self, message, event):
|
||||
if not headphones.CONFIG.PUSHALOT_ENABLED:
|
||||
return
|
||||
@@ -501,9 +491,9 @@ class PUSHALOT(object):
|
||||
'Body': message.encode("utf-8")}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
@@ -512,14 +502,14 @@ class PUSHALOT(object):
|
||||
logger.debug(u"Pushalot response body: %r" % response.read())
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushalot notifications sent.")
|
||||
return True
|
||||
logger.info(u"Pushalot notifications sent.")
|
||||
return True
|
||||
elif request_status == 410:
|
||||
logger.info(u"Pushalot auth failed: %s" % response.reason)
|
||||
return False
|
||||
logger.info(u"Pushalot auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Pushalot notification failed.")
|
||||
return False
|
||||
logger.info(u"Pushalot notification failed.")
|
||||
return False
|
||||
|
||||
|
||||
class Synoindex(object):
|
||||
@@ -533,7 +523,8 @@ class Synoindex(object):
|
||||
path = os.path.abspath(path)
|
||||
|
||||
if not self.util_exists():
|
||||
logger.warn("Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
logger.warn(
|
||||
"Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
return
|
||||
|
||||
if os.path.isfile(path):
|
||||
@@ -541,15 +532,17 @@ class Synoindex(object):
|
||||
elif os.path.isdir(path):
|
||||
cmd_arg = '-A'
|
||||
else:
|
||||
logger.warn("Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
logger.warn(
|
||||
"Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
return
|
||||
|
||||
cmd = [self.util_loc, cmd_arg, path]
|
||||
logger.info("Calling synoindex command: %s" % str(cmd))
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
cwd=headphones.PROG_DIR)
|
||||
out, error = p.communicate()
|
||||
#synoindex never returns any codes other than '0', highly irritating
|
||||
# synoindex never returns any codes other than '0', highly irritating
|
||||
except OSError, e:
|
||||
logger.warn("Error sending notification: %s" % str(e))
|
||||
|
||||
@@ -560,7 +553,6 @@ class Synoindex(object):
|
||||
|
||||
|
||||
class PUSHOVER(object):
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = headphones.CONFIG.PUSHOVER_ENABLED
|
||||
self.keys = headphones.CONFIG.PUSHOVER_KEYS
|
||||
@@ -578,7 +570,7 @@ class PUSHOVER(object):
|
||||
if not headphones.CONFIG.PUSHOVER_ENABLED:
|
||||
return
|
||||
|
||||
http_handler = HTTPSConnection("api.pushover.net")
|
||||
url = "https://api.pushover.net/1/messages.json"
|
||||
|
||||
data = {'token': self.application_token,
|
||||
'user': headphones.CONFIG.PUSHOVER_KEYS,
|
||||
@@ -586,28 +578,19 @@ class PUSHOVER(object):
|
||||
'message': message.encode("utf-8"),
|
||||
'priority': headphones.CONFIG.PUSHOVER_PRIORITY}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/1/messages.json",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
logger.debug(u"Pushover response status: %r" % request_status)
|
||||
logger.debug(u"Pushover response headers: %r" % response.getheaders())
|
||||
logger.debug(u"Pushover response body: %r" % response.read())
|
||||
headers = {'Content-type': "application/x-www-form-urlencoded"}
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushover notifications sent.")
|
||||
return True
|
||||
elif request_status >= 400 and request_status < 500:
|
||||
logger.info(u"Pushover request failed: %s" % response.reason)
|
||||
return False
|
||||
response = request.request_response(url, method="POST", headers=headers, data=data)
|
||||
|
||||
if response:
|
||||
logger.info(u"Pushover notifications sent.")
|
||||
return True
|
||||
else:
|
||||
logger.info(u"Pushover notification failed.")
|
||||
return False
|
||||
logger.error(u"Pushover notification failed.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
@@ -619,7 +602,6 @@ class PUSHOVER(object):
|
||||
|
||||
|
||||
class TwitterNotifier(object):
|
||||
|
||||
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
|
||||
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
|
||||
AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
|
||||
@@ -631,14 +613,17 @@ class TwitterNotifier(object):
|
||||
|
||||
def notify_snatch(self, title):
|
||||
if headphones.CONFIG.TWITTER_ONSNATCH:
|
||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
self._notifyTwitter(
|
||||
common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
|
||||
def notify_download(self, title):
|
||||
if headphones.CONFIG.TWITTER_ENABLED:
|
||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
self._notifyTwitter(common.notifyStrings[
|
||||
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
|
||||
def test_notify(self):
|
||||
return self._notifyTwitter("This is a test notification from Headphones at " + helpers.now(), force=True)
|
||||
return self._notifyTwitter(
|
||||
"This is a test notification from Headphones at " + helpers.now(), force=True)
|
||||
|
||||
def _get_authorization(self):
|
||||
|
||||
@@ -675,7 +660,8 @@ class TwitterNotifier(object):
|
||||
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
||||
oauth_client = oauth.Client(oauth_consumer, token)
|
||||
logger.info('oauth_client: ' + str(oauth_client))
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST',
|
||||
body='oauth_verifier=%s' % key)
|
||||
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
||||
|
||||
access_token = dict(parse_qsl(content))
|
||||
@@ -683,7 +669,8 @@ class TwitterNotifier(object):
|
||||
|
||||
logger.info('resp[status] = ' + str(resp['status']))
|
||||
if resp['status'] != '200':
|
||||
logger.info('The request for a token with did not succeed: ' + str(resp['status']), logger.ERROR)
|
||||
logger.info('The request for a token with did not succeed: ' + str(resp['status']),
|
||||
logger.ERROR)
|
||||
return False
|
||||
else:
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
|
||||
@@ -721,29 +708,37 @@ class TwitterNotifier(object):
|
||||
|
||||
|
||||
class OSX_NOTIFY(object):
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.objc = __import__("objc")
|
||||
self.AppKit = __import__("AppKit")
|
||||
except:
|
||||
logger.warn('OS X Notification: Cannot import objc or AppKit')
|
||||
return False
|
||||
|
||||
def swizzle(self, cls, SEL, func):
|
||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
||||
old_IMP = getattr(cls, SEL, None)
|
||||
if old_IMP is None:
|
||||
old_IMP = cls.instanceMethodForSelector_(SEL)
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
return func(self, old_IMP, *args, **kwargs)
|
||||
new_IMP = self.objc.selector(wrapper, selector=old_IMP.selector,
|
||||
signature=old_IMP.signature)
|
||||
self.objc.classAddMethod(cls, SEL, new_IMP)
|
||||
|
||||
new_IMP = self.objc.selector(
|
||||
wrapper,
|
||||
selector=old_IMP.selector,
|
||||
signature=old_IMP.signature
|
||||
)
|
||||
self.objc.classAddMethod(cls, SEL.encode(), new_IMP)
|
||||
|
||||
def notify(self, title, subtitle=None, text=None, sound=True, image=None):
|
||||
|
||||
try:
|
||||
self.swizzle(self.objc.lookUpClass('NSBundle'),
|
||||
b'bundleIdentifier',
|
||||
self.swizzled_bundleIdentifier)
|
||||
self.swizzle(
|
||||
self.objc.lookUpClass('NSBundle'),
|
||||
'bundleIdentifier',
|
||||
self.swizzled_bundleIdentifier
|
||||
)
|
||||
|
||||
NSUserNotification = self.objc.lookUpClass('NSUserNotification')
|
||||
NSUserNotificationCenter = self.objc.lookUpClass('NSUserNotificationCenter')
|
||||
@@ -765,7 +760,7 @@ class OSX_NOTIFY(object):
|
||||
if image:
|
||||
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
|
||||
notification.setContentImage_(source_img)
|
||||
#notification.set_identityImage_(source_img)
|
||||
# notification.set_identityImage_(source_img)
|
||||
notification.setHasActionButton_(False)
|
||||
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
@@ -783,7 +778,6 @@ class OSX_NOTIFY(object):
|
||||
|
||||
|
||||
class BOXCAR(object):
|
||||
|
||||
def __init__(self):
|
||||
self.url = 'https://new.boxcar.io/api/notifications'
|
||||
|
||||
@@ -797,7 +791,7 @@ class BOXCAR(object):
|
||||
'notification[title]': title.encode('utf-8'),
|
||||
'notification[long_message]': message.encode('utf-8'),
|
||||
'notification[sound]': "done"
|
||||
})
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
handle = urllib2.urlopen(req, data)
|
||||
@@ -810,7 +804,6 @@ class BOXCAR(object):
|
||||
|
||||
|
||||
class SubSonicNotifier(object):
|
||||
|
||||
def __init__(self):
|
||||
self.host = headphones.CONFIG.SUBSONIC_HOST
|
||||
self.username = headphones.CONFIG.SUBSONIC_USERNAME
|
||||
@@ -826,4 +819,72 @@ class SubSonicNotifier(object):
|
||||
|
||||
# Invoke request
|
||||
request.request_response(self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
auth=(self.username, self.password))
|
||||
|
||||
|
||||
class Email(object):
|
||||
def notify(self, subject, message):
|
||||
|
||||
message = MIMEText(message, 'plain', "utf-8")
|
||||
message['Subject'] = subject
|
||||
message['From'] = email.utils.formataddr(('Headphones', headphones.CONFIG.EMAIL_FROM))
|
||||
message['To'] = headphones.CONFIG.EMAIL_TO
|
||||
|
||||
try:
|
||||
if headphones.CONFIG.EMAIL_SSL:
|
||||
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
else:
|
||||
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
|
||||
if headphones.CONFIG.EMAIL_TLS:
|
||||
mailserver.starttls()
|
||||
|
||||
mailserver.ehlo()
|
||||
|
||||
if headphones.CONFIG.EMAIL_SMTP_USER:
|
||||
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PASSWORD)
|
||||
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO,
|
||||
message.as_string())
|
||||
mailserver.quit()
|
||||
return True
|
||||
|
||||
except Exception, e:
|
||||
logger.warn('Error sending Email: %s' % e)
|
||||
return False
|
||||
|
||||
|
||||
class TELEGRAM(object):
|
||||
|
||||
def notify(self, message, status):
|
||||
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
||||
return
|
||||
|
||||
import requests
|
||||
|
||||
TELEGRAM_API = "https://api.telegram.org/bot%s/%s"
|
||||
|
||||
# Get configuration data
|
||||
token = headphones.CONFIG.TELEGRAM_TOKEN
|
||||
userid = headphones.CONFIG.TELEGRAM_USERID
|
||||
|
||||
# Construct message
|
||||
payload = {'chat_id': userid, 'text': status + ': ' + message}
|
||||
|
||||
# Send message to user using Telegram's Bot API
|
||||
try:
|
||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"), data=payload)
|
||||
except Exception, e:
|
||||
logger.info(u'Telegram notify failed: ' + str(e))
|
||||
|
||||
# Error logging
|
||||
sent_successfuly = True
|
||||
if not response.status_code == 200:
|
||||
logger.info(u'Could not send notification to TelegramBot (token=%s). Response: [%s]', (token, response.text))
|
||||
sent_successfuly = False
|
||||
|
||||
logger.info(u"Telegram notifications sent.")
|
||||
return sent_successfuly
|
||||
|
||||
+39
-23
@@ -19,43 +19,45 @@
|
||||
# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import httplib
|
||||
|
||||
import headphones
|
||||
|
||||
from base64 import standard_b64encode
|
||||
import httplib
|
||||
import xmlrpclib
|
||||
|
||||
import headphones
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def sendNZB(nzb):
|
||||
|
||||
addToTop = False
|
||||
nzbgetXMLrpc = "%(username)s:%(password)s@%(host)s/xmlrpc"
|
||||
nzbgetXMLrpc = "%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc"
|
||||
|
||||
if headphones.CONFIG.NZBGET_HOST is None:
|
||||
if not headphones.CONFIG.NZBGET_HOST:
|
||||
logger.error(u"No NZBget host found in configuration. Please configure it.")
|
||||
return False
|
||||
|
||||
if headphones.CONFIG.NZBGET_HOST.startswith('https://'):
|
||||
nzbgetXMLrpc = 'https://' + nzbgetXMLrpc
|
||||
headphones.CONFIG.NZBGET_HOST.replace('https://', '', 1)
|
||||
protocol = 'https'
|
||||
host = headphones.CONFIG.NZBGET_HOST.replace('https://', '', 1)
|
||||
else:
|
||||
nzbgetXMLrpc = 'http://' + nzbgetXMLrpc
|
||||
headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1)
|
||||
protocol = 'http'
|
||||
host = headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1)
|
||||
|
||||
url = nzbgetXMLrpc % {"host": headphones.CONFIG.NZBGET_HOST, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD}
|
||||
url = nzbgetXMLrpc % {"protocol": protocol, "host": host,
|
||||
"username": headphones.CONFIG.NZBGET_USERNAME,
|
||||
"password": headphones.CONFIG.NZBGET_PASSWORD}
|
||||
|
||||
nzbGetRPC = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if nzbGetRPC.writelog("INFO", "headphones connected to drop of %s any moment now." % (nzb.name + ".nzb")):
|
||||
if nzbGetRPC.writelog("INFO", "headphones connected to drop of %s any moment now." % (
|
||||
nzb.name + ".nzb")):
|
||||
logger.debug(u"Successfully connected to NZBget")
|
||||
else:
|
||||
logger.info(u"Successfully connected to NZBget, but unable to send a message" % (nzb.name + ".nzb"))
|
||||
logger.info(u"Successfully connected to NZBget, but unable to send a message" % (
|
||||
nzb.name + ".nzb"))
|
||||
|
||||
except httplib.socket.error:
|
||||
logger.error(u"Please check your NZBget host and port (if it is running). NZBget is not responding to this combination")
|
||||
logger.error(
|
||||
u"Please check your NZBget host and port (if it is running). NZBget is not responding to this combination")
|
||||
return False
|
||||
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
@@ -82,7 +84,9 @@ def sendNZB(nzb):
|
||||
nzbget_version = int(nzbget_version_str[:nzbget_version_str.find(".")])
|
||||
if nzbget_version == 0:
|
||||
if nzbcontent64 is not None:
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, addToTop, nzbcontent64)
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY, addToTop,
|
||||
nzbcontent64)
|
||||
else:
|
||||
# from headphones.common.providers.generic import GenericProvider
|
||||
# if nzb.resultType == "nzb":
|
||||
@@ -95,24 +99,35 @@ def sendNZB(nzb):
|
||||
return False
|
||||
elif nzbget_version == 12:
|
||||
if nzbcontent64 is not None:
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbcontent64, False, dupekey, dupescore, "score")
|
||||
else:
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzb.url, False, dupekey, dupescore, "score")
|
||||
# v13+ has a new combined append method that accepts both (url and content)
|
||||
# also the return value has changed from boolean to integer
|
||||
# (Positive number representing NZBID of the queue item. 0 and negative numbers represent error codes.)
|
||||
elif nzbget_version >= 13:
|
||||
nzbget_result = True if nzbGetRPC.append(nzb.name + ".nzb", nzbcontent64 if nzbcontent64 is not None else nzb.url,
|
||||
headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False, False, dupekey, dupescore,
|
||||
nzbget_result = True if nzbGetRPC.append(nzb.name + ".nzb",
|
||||
nzbcontent64 if nzbcontent64 is not None else nzb.url,
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
False, dupekey, dupescore,
|
||||
"score") > 0 else False
|
||||
else:
|
||||
if nzbcontent64 is not None:
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbcontent64)
|
||||
else:
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzb.url)
|
||||
|
||||
if nzbget_result:
|
||||
@@ -122,5 +137,6 @@ def sendNZB(nzb):
|
||||
logger.error(u"NZBget could not add %s to the queue" % (nzb.name + ".nzb"))
|
||||
return False
|
||||
except:
|
||||
logger.error(u"Connect Error to NZBget: could not add %s to the queue" % (nzb.name + ".nzb"))
|
||||
logger.error(
|
||||
u"Connect Error to NZBget: could not add %s to the queue" % (nzb.name + ".nzb"))
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
# encoding=utf8
|
||||
# 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/>.
|
||||
"""
|
||||
Path pattern substitution module, see details below for syntax.
|
||||
|
||||
The pattern matching is loosely based on foobar2000 pattern syntax,
|
||||
i.e. the notion of escaping characters with \' and optional elements
|
||||
enclosed in square brackets [] is taken from there while the
|
||||
substitution variable names are Perl-ish or sh-ish. The following
|
||||
syntax elements are supported:
|
||||
* escaped literal strings, that is everything that is enclosed
|
||||
within single quotes (like 'this');
|
||||
* substitution variables, which start with dollar sign ($) and
|
||||
extend until next non-alphanumeric+underscore character
|
||||
(like $This and $5_that).
|
||||
* optional elements enclosed in curly braces, which render
|
||||
nonempty value only if any variable or optional inside returned
|
||||
nonempty value, ignoring literals (like {'{'$That'}'}).
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from enum import Enum
|
||||
|
||||
__author__ = "Andrzej Ciarkowski <andrzej.ciarkowski@gmail.com>"
|
||||
|
||||
|
||||
class _PatternElement(object):
|
||||
'''ABC for hierarchy of path name renderer pattern elements.'''
|
||||
def render(self, replacement):
|
||||
# type: (Mapping[str,str]) -> str
|
||||
'''Format this _PatternElement into string using provided substitution dictionary.'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
class _Generator(_PatternElement):
|
||||
# pylint: disable=abstract-method
|
||||
'''Tagging interface for "content-generating" elements like replacement or optional block.'''
|
||||
pass
|
||||
|
||||
|
||||
class _Replacement(_Generator):
|
||||
'''Replacement variable, eg. $title.'''
|
||||
def __init__(self, pattern):
|
||||
# type: (str)
|
||||
self._pattern = pattern
|
||||
|
||||
def render(self, replacement):
|
||||
# type: (Mapping[str,str]) -> str
|
||||
res = replacement.get(self._pattern, self._pattern)
|
||||
if res is None:
|
||||
return ''
|
||||
else:
|
||||
return res
|
||||
|
||||
def __str__(self):
|
||||
return self._pattern
|
||||
|
||||
@property
|
||||
def pattern(self):
|
||||
return self._pattern
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _Replacement) and \
|
||||
self._pattern == other.pattern
|
||||
|
||||
|
||||
class _LiteralText(_PatternElement):
|
||||
'''Just a plain piece of text to be rendered "as is".'''
|
||||
def __init__(self, text):
|
||||
# type: (str)
|
||||
self._text = text
|
||||
|
||||
def render(self, replacement):
|
||||
# type: (Mapping[str,str]) -> str
|
||||
return self._text
|
||||
|
||||
def __str__(self):
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._text
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, _LiteralText) and self._text == other.text
|
||||
|
||||
|
||||
class _OptionalBlock(_Generator):
|
||||
'''Optional block will render its contents only if any _Generator in its scope did return non-empty result.'''
|
||||
|
||||
def __init__(self, scope):
|
||||
# type: ([_PatternElement])
|
||||
self._scope = scope
|
||||
|
||||
def render(self, replacement):
|
||||
# type: (Mapping[str,str]) -> str
|
||||
res = [(isinstance(x, _Generator), x.render(replacement)) for x in self._scope]
|
||||
if any((t[0] and t[1] is not None and len(t[1]) != 0) for t in res):
|
||||
return u"".join(t[1] for t in res)
|
||||
else:
|
||||
return u""
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
:type other: _OptionalBlock
|
||||
"""
|
||||
return isinstance(other, _OptionalBlock) and self._scope == other._scope
|
||||
|
||||
|
||||
_OPTIONAL_START = u'{'
|
||||
_OPTIONAL_END = u'}'
|
||||
_ESCAPE_CHAR = u'\''
|
||||
_REPLACEMENT_START = u'$'
|
||||
|
||||
|
||||
def _is_replacement_valid(c):
|
||||
# type: (str) -> bool
|
||||
return c.isalnum() or c == u'_'
|
||||
|
||||
|
||||
class _State(Enum):
|
||||
LITERAL = 0
|
||||
ESCAPE = 1
|
||||
REPLACEMENT = 2
|
||||
|
||||
|
||||
def _append_literal(scope, text):
|
||||
# type: ([_PatternElement], str) -> None
|
||||
'''Append literal text to the scope BUT ONLY if it's not an empty string.'''
|
||||
if len(text) == 0:
|
||||
return
|
||||
scope.append(_LiteralText(text))
|
||||
|
||||
|
||||
class Warnings(Enum):
|
||||
'''Pattern parsing warnings, as stored withing warnings property of Pattern object after parsing.'''
|
||||
UNCLOSED_ESCAPE = 'Warnings.UNCLOSED_ESCAPE'
|
||||
UNCLOSED_OPTIONAL = 'Warnings.UNCLOSED_OPTIONAL'
|
||||
|
||||
|
||||
def _parse_pattern(pattern, warnings):
|
||||
# type: (str,MutableSet[Warnings]) -> [_PatternElement]
|
||||
'''Parse path pattern text into list of _PatternElements, put warnings into the provided set.'''
|
||||
start = 0 # index of current state start char
|
||||
root_scope = [] # here our _PatternElements will reside
|
||||
scope_stack = [root_scope] # stack so that we can return to the outer scope
|
||||
scope = root_scope # pointer to the current list for _OptionalBlock
|
||||
inside_optional = 0 # nesting level of _OptionalBlocks
|
||||
state = _State.LITERAL # current state
|
||||
for i, c in enumerate(pattern):
|
||||
if state is _State.ESCAPE:
|
||||
if c != _ESCAPE_CHAR:
|
||||
# only escape char can get us out of _State.ESCAPE
|
||||
continue
|
||||
_append_literal(scope, pattern[start + 1:i])
|
||||
state = _State.LITERAL
|
||||
start = i + 1
|
||||
# after exiting _State.ESCAPE on escape char no more processing of c
|
||||
continue
|
||||
if state is _State.REPLACEMENT:
|
||||
if _is_replacement_valid(c):
|
||||
# only replacement invalid can get us out _State.REPLACEMENT
|
||||
continue
|
||||
scope.append(_Replacement(pattern[start:i]))
|
||||
state = _State.LITERAL
|
||||
start = i
|
||||
# intentional fall-through to _State.LITERAL
|
||||
assert state is _State.LITERAL
|
||||
if c == _ESCAPE_CHAR:
|
||||
_append_literal(scope, pattern[start:i])
|
||||
state = _State.ESCAPE
|
||||
start = i
|
||||
# no more processing to escape char c
|
||||
continue
|
||||
if c == _REPLACEMENT_START:
|
||||
_append_literal(scope, pattern[start:i])
|
||||
state = _State.REPLACEMENT
|
||||
start = i
|
||||
# no more processing to replacement char c
|
||||
continue
|
||||
if c == _OPTIONAL_START:
|
||||
_append_literal(scope, pattern[start:i])
|
||||
inside_optional += 1
|
||||
new_scope = []
|
||||
scope_stack.append(new_scope)
|
||||
scope = new_scope
|
||||
start = i + 1
|
||||
continue
|
||||
if c == _OPTIONAL_END:
|
||||
if inside_optional == 0:
|
||||
# no optional block to end, just treat as literal text
|
||||
continue
|
||||
inside_optional -= 1
|
||||
_append_literal(scope, pattern[start:i])
|
||||
scope_stack.pop()
|
||||
prev_scope = scope_stack[-1]
|
||||
prev_scope.append(_OptionalBlock(scope))
|
||||
scope = prev_scope
|
||||
start = i + 1
|
||||
# fi
|
||||
# done
|
||||
if state is _State.ESCAPE:
|
||||
warnings.add(Warnings.UNCLOSED_ESCAPE)
|
||||
if inside_optional != 0:
|
||||
warnings.add(Warnings.UNCLOSED_OPTIONAL)
|
||||
if state is _State.REPLACEMENT:
|
||||
root_scope.append(_Replacement(pattern[start:]))
|
||||
else:
|
||||
# don't care about unclosed elements :P
|
||||
_append_literal(root_scope, pattern[start:])
|
||||
return root_scope
|
||||
|
||||
|
||||
class Pattern(object):
|
||||
'''Stores preparsed rename pattern for repeated use.
|
||||
|
||||
If using the same pattern repeatedly it is much more effective
|
||||
to parse the pattern into Pattern object and use it instead of
|
||||
parsing the textual pattern on each substitution. To use Pattern
|
||||
object for substitution simply call it as it was function
|
||||
providing dictionary as an argument (see __call__()).'''
|
||||
|
||||
def __init__(self, pattern):
|
||||
# type: (str)
|
||||
self._warnings = set()
|
||||
self._pattern = _parse_pattern(pattern, self._warnings)
|
||||
|
||||
def __call__(self, replacement):
|
||||
# type: (Mapping[str,str]) -> str
|
||||
'''Execute path rendering/substitution based on replacement dictionary.'''
|
||||
return u"".join(p.render(replacement) for p in self._pattern)
|
||||
|
||||
def _get_warnings(self):
|
||||
# type: () -> str
|
||||
'''Getter for warnings property.'''
|
||||
return self._warnings
|
||||
|
||||
warnings = property(_get_warnings, doc="Access warnings raised during pattern parsing")
|
||||
|
||||
|
||||
def render(pattern, replacement):
|
||||
# type: (str, Mapping[str,str]) -> (str, AbstractSet[Warnings])
|
||||
'''Render path name based on replacement pattern and dictionary.'''
|
||||
p = Pattern(pattern)
|
||||
return p(replacement), p.warnings
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# primitive test ;)
|
||||
p = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
|
||||
d = {'$Disc': '', '$Track': '05', '$Artist': u'Grzegżółka', '$Title': u'Błona kapłona', '$Year': '2019'}
|
||||
assert p(d) == u"05 - Grzegżółka - Błona kapłona [2019]"
|
||||
@@ -0,0 +1,96 @@
|
||||
# encoding=utf8
|
||||
# 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/>.
|
||||
"""
|
||||
Test module for pathrender.
|
||||
"""
|
||||
import headphones.pathrender as _pr
|
||||
from headphones.pathrender import Pattern, Warnings
|
||||
|
||||
from unittestcompat import TestCase
|
||||
|
||||
|
||||
__author__ = "Andrzej Ciarkowski <andrzej.ciarkowski@gmail.com>"
|
||||
|
||||
|
||||
class PathRenderTest(TestCase):
|
||||
"""
|
||||
Tests for pathrender module.
|
||||
"""
|
||||
|
||||
def test_parsing(self):
|
||||
"""pathrender: pattern parsing"""
|
||||
pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
|
||||
expected = [
|
||||
_pr._OptionalBlock([
|
||||
_pr._Replacement(u"$Disc"),
|
||||
_pr._LiteralText(u".")
|
||||
]),
|
||||
_pr._Replacement(u"$Track"),
|
||||
_pr._LiteralText(u" - "),
|
||||
_pr._Replacement(u"$Artist"),
|
||||
_pr._LiteralText(u" - "),
|
||||
_pr._Replacement(u"$Title"),
|
||||
_pr._OptionalBlock([
|
||||
_pr._LiteralText(u" ["),
|
||||
_pr._Replacement(u"$Year"),
|
||||
_pr._LiteralText(u"]")
|
||||
])
|
||||
]
|
||||
self.assertEqual(expected, pattern._pattern)
|
||||
self.assertItemsEqual([], pattern.warnings)
|
||||
|
||||
def test_parsing_warnings(self):
|
||||
"""pathrender: pattern parsing with warnings"""
|
||||
pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]")
|
||||
self.assertEqual(set([Warnings.UNCLOSED_OPTIONAL]), pattern.warnings)
|
||||
pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]'}")
|
||||
self.assertEqual(set([Warnings.UNCLOSED_ESCAPE, Warnings.UNCLOSED_OPTIONAL]), pattern.warnings)
|
||||
|
||||
def test_replacement(self):
|
||||
"""pathrender: _Replacement variable substitution"""
|
||||
r = _pr._Replacement(u"$Title")
|
||||
subst = {'$Title': 'foo', '$Track': 'bar'}
|
||||
res = r.render(subst)
|
||||
self.assertEqual(res, u'foo', 'check valid replacement')
|
||||
subst = {}
|
||||
res = r.render(subst)
|
||||
self.assertEqual(res, u'$Title', 'check missing replacement')
|
||||
subst = {'$Title': None}
|
||||
res = r.render(subst)
|
||||
self.assertEqual(res, '', 'check render() works with None')
|
||||
|
||||
def test_literal(self):
|
||||
"""pathrender: _Literal text rendering"""
|
||||
l = _pr._LiteralText(u"foo")
|
||||
subst = {'$foo': 'bar'}
|
||||
res = l.render(subst)
|
||||
self.assertEqual(res, 'foo')
|
||||
|
||||
def test_optional(self):
|
||||
"""pathrender: _OptionalBlock element processing"""
|
||||
o = _pr._OptionalBlock([
|
||||
_pr._Replacement(u"$Title"),
|
||||
_pr._LiteralText(u".foobar")
|
||||
])
|
||||
subst = {'$Title': 'foo', '$Track': 'bar'}
|
||||
res = o.render(subst)
|
||||
self.assertEqual(res, u'foo.foobar', 'check non-empty replacement')
|
||||
subst = {'$Title': ''}
|
||||
res = o.render(subst)
|
||||
self.assertEqual(res, '', 'check empty replacement')
|
||||
subst = {'$Title': None}
|
||||
res = o.render(subst)
|
||||
self.assertEqual(res, '', 'check render() works with None')
|
||||
+369
-287
File diff suppressed because it is too large
Load Diff
+20
-6
@@ -13,16 +13,20 @@
|
||||
# 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 logger
|
||||
|
||||
from xml.dom import minidom
|
||||
from bs4 import BeautifulSoup
|
||||
import collections
|
||||
|
||||
import sys
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from headphones import logger
|
||||
import feedparser
|
||||
import headphones
|
||||
import headphones.lock
|
||||
import collections
|
||||
|
||||
|
||||
# Disable SSL certificate warnings. We have our own handling
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
# Dictionary with last request times, for rate limiting.
|
||||
last_requests = collections.defaultdict(int)
|
||||
@@ -51,6 +55,14 @@ def request_response(url, method="get", auto_raise=True,
|
||||
# pose a security issue!
|
||||
kwargs["verify"] = bool(headphones.CONFIG.VERIFY_SSL_CERT)
|
||||
|
||||
# This fix is put in place for systems with broken SSL (like QNAP)
|
||||
if not headphones.CONFIG.VERIFY_SSL_CERT and sys.version_info >= (2, 7, 9):
|
||||
try:
|
||||
import ssl
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
except:
|
||||
pass
|
||||
|
||||
# Map method to the request.XXX method. This is a simple hack, but it
|
||||
# allows requests to apply more magic per method. See lib/requests/api.py.
|
||||
request_method = getattr(requests, method.lower())
|
||||
@@ -95,7 +107,8 @@ def request_response(url, method="get", auto_raise=True,
|
||||
"host is up and running.")
|
||||
except requests.Timeout:
|
||||
logger.error(
|
||||
"Request timed out. The remote host did not respond timely.")
|
||||
"Request timed out. The remote host did not respond in a timely "
|
||||
"manner.")
|
||||
except requests.HTTPError as e:
|
||||
if e.response is not None:
|
||||
if e.response.status_code >= 500:
|
||||
@@ -206,7 +219,8 @@ def server_message(response):
|
||||
message = None
|
||||
|
||||
# First attempt is to 'read' the response as HTML
|
||||
if "text/html" in response.headers.get("content-type"):
|
||||
if response.headers.get("content-type") and \
|
||||
"text/html" in response.headers.get("content-type"):
|
||||
try:
|
||||
soup = BeautifulSoup(response.content, "html5lib")
|
||||
except Exception:
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import urllib
|
||||
import time
|
||||
from urlparse import urlparse
|
||||
import re
|
||||
|
||||
import requests as requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import headphones
|
||||
from headphones import logger
|
||||
|
||||
|
||||
class Rutracker(object):
|
||||
def __init__(self):
|
||||
self.session = requests.session()
|
||||
self.timeout = 60
|
||||
self.loggedin = False
|
||||
self.maxsize = 0
|
||||
self.search_referer = 'http://rutracker.org/forum/tracker.php'
|
||||
|
||||
def logged_in(self):
|
||||
return self.loggedin
|
||||
|
||||
def still_logged_in(self, html):
|
||||
if not html or "action=\"http://login.rutracker.org/forum/login.php\">" in html:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Logs in user
|
||||
"""
|
||||
|
||||
loginpage = 'http://login.rutracker.org/forum/login.php'
|
||||
post_params = {
|
||||
'login_username': headphones.CONFIG.RUTRACKER_USER,
|
||||
'login_password': headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||
'login': b'\xc2\xf5\xee\xe4' # '%C2%F5%EE%E4'
|
||||
}
|
||||
|
||||
logger.info("Attempting to log in to rutracker...")
|
||||
|
||||
try:
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||
# try again
|
||||
if not self.has_bb_data_cookie(r):
|
||||
time.sleep(10)
|
||||
r = self.session.post(loginpage, data=post_params, timeout=self.timeout, allow_redirects=False)
|
||||
if self.has_bb_data_cookie(r):
|
||||
self.loggedin = True
|
||||
logger.info("Successfully logged in to rutracker")
|
||||
else:
|
||||
logger.error(
|
||||
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
except Exception as e:
|
||||
logger.error("Unknown error logging in to rutracker: %s" % e)
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
|
||||
def has_bb_data_cookie(self, response):
|
||||
if 'bb_data' in response.cookies.keys():
|
||||
return True
|
||||
# Rutracker randomly send a 302 redirect code, cookie may be present in response history
|
||||
return next(('bb_data' in r.cookies.keys() for r in response.history), False)
|
||||
|
||||
def searchurl(self, artist, album, year, format):
|
||||
"""
|
||||
Return the search url
|
||||
"""
|
||||
# Build search url
|
||||
searchterm = ''
|
||||
if artist != 'Various Artists':
|
||||
searchterm = artist
|
||||
searchterm = searchterm + ' '
|
||||
searchterm = searchterm + album
|
||||
searchterm = searchterm + ' '
|
||||
searchterm = searchterm + year
|
||||
|
||||
if format == 'lossless':
|
||||
format = '+lossless'
|
||||
self.maxsize = 10000000000
|
||||
elif format == 'lossless+mp3':
|
||||
format = '+lossless||mp3||aac'
|
||||
self.maxsize = 10000000000
|
||||
else:
|
||||
format = '+mp3||aac'
|
||||
self.maxsize = 300000000
|
||||
|
||||
# sort by size, descending.
|
||||
sort = '&o=7&s=2'
|
||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||
logger.info("Searching rutracker using term: %s", searchterm)
|
||||
return searchurl
|
||||
|
||||
def search(self, searchurl):
|
||||
"""
|
||||
Parse the search results and return valid torrent list
|
||||
"""
|
||||
try:
|
||||
headers = {'Referer': self.search_referer}
|
||||
r = self.session.get(url=searchurl, headers=headers, timeout=self.timeout)
|
||||
soup = BeautifulSoup(r.content, 'html5lib')
|
||||
|
||||
# Debug
|
||||
# logger.debug (soup.prettify())
|
||||
|
||||
# Check if still logged in
|
||||
if not self.still_logged_in(soup):
|
||||
self.login()
|
||||
r = self.session.get(url=searchurl, timeout=self.timeout)
|
||||
soup = BeautifulSoup(r.content, 'html5lib')
|
||||
if not self.still_logged_in(soup):
|
||||
logger.error("Error getting rutracker data")
|
||||
return None
|
||||
|
||||
# Process
|
||||
rulist = []
|
||||
i = soup.find('table', id='tor-tbl')
|
||||
if not i:
|
||||
logger.info("No valid results found from rutracker")
|
||||
return None
|
||||
minimumseeders = int(headphones.CONFIG.NUMBEROFSEEDERS) - 1
|
||||
|
||||
for item in zip(i.find_all(class_='hl-tags'), i.find_all(class_='dl-stub'),
|
||||
i.find_all(class_='seedmed')):
|
||||
title = item[0].get_text()
|
||||
url = item[1].get('href')
|
||||
size_formatted = item[1].get_text()[:-2]
|
||||
seeds = item[2].get_text()
|
||||
size_parts = size_formatted.split()
|
||||
size = float(size_parts[0])
|
||||
|
||||
if size_parts[1] == 'KB':
|
||||
size *= 1024
|
||||
if size_parts[1] == 'MB':
|
||||
size *= 1024 ** 2
|
||||
if size_parts[1] == 'GB':
|
||||
size *= 1024 ** 3
|
||||
if size_parts[1] == 'TB':
|
||||
size *= 1024 ** 4
|
||||
|
||||
if size < self.maxsize and minimumseeders < int(seeds):
|
||||
logger.info('Found %s. Size: %s' % (title, size_formatted))
|
||||
# Torrent topic page
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])[
|
||||
't']
|
||||
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
|
||||
rulist.append((title, size, topicurl, 'rutracker.org', 'torrent', True))
|
||||
else:
|
||||
logger.info("%s is larger than the maxsize or has too little seeders for this category, "
|
||||
"skipping. (Size: %i bytes, Seeders: %i)" % (title, size, int(seeds)))
|
||||
|
||||
if not rulist:
|
||||
logger.info("No valid results found from rutracker")
|
||||
|
||||
return rulist
|
||||
|
||||
except Exception as e:
|
||||
logger.error("An unknown error occurred in the rutracker parser: %s" % e)
|
||||
return None
|
||||
|
||||
def get_torrent_data(self, url):
|
||||
"""
|
||||
return the .torrent data
|
||||
"""
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
downloadurl = 'http://dl.rutracker.org/forum/dl.php?t=' + torrent_id
|
||||
cookie = {'bb_dl': torrent_id}
|
||||
try:
|
||||
headers = {'Referer': url}
|
||||
r = self.session.post(url=downloadurl, cookies=cookie, headers=headers,
|
||||
timeout=self.timeout)
|
||||
return r.content
|
||||
except Exception as e:
|
||||
logger.error('Error getting torrent: %s', e)
|
||||
return False
|
||||
|
||||
# TODO get this working in utorrent.py
|
||||
def utorrent_add_file(self, data):
|
||||
host = headphones.CONFIG.UTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
if host.endswith('/gui'):
|
||||
host = host[:-4]
|
||||
|
||||
base_url = host
|
||||
url = base_url + '/gui/'
|
||||
self.session.auth = (headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD)
|
||||
|
||||
try:
|
||||
r = self.session.get(url + 'token.html')
|
||||
except Exception as e:
|
||||
logger.error('Error getting token: %s', e)
|
||||
return
|
||||
|
||||
if r.status_code == 401:
|
||||
logger.debug('Error reaching utorrent')
|
||||
return
|
||||
|
||||
regex = re.search(r'.+>([^<]+)</div></html>', r.text)
|
||||
if regex is None:
|
||||
logger.debug('Error reading token')
|
||||
return
|
||||
|
||||
self.session.params = {'token': regex.group(1)}
|
||||
files = {'torrent_file': ("", data)}
|
||||
|
||||
try:
|
||||
self.session.post(url, params={'action': 'add-file'}, files=files)
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to utorrent %s', e)
|
||||
+50
-95
@@ -13,26 +13,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#####################################
|
||||
## Stolen from Sick-Beard's sab.py ##
|
||||
#####################################
|
||||
###################################
|
||||
# Stolen from Sick-Beard's sab.py #
|
||||
###################################
|
||||
|
||||
import MultipartPostHandler
|
||||
import headphones
|
||||
import cookielib
|
||||
import urllib2
|
||||
import httplib
|
||||
import urllib
|
||||
import ast
|
||||
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger
|
||||
from headphones import helpers
|
||||
from headphones import logger, helpers, request
|
||||
|
||||
|
||||
def sendNZB(nzb):
|
||||
def sab_api_call(request_type=None, params={}, **kwargs):
|
||||
if not headphones.CONFIG.SAB_HOST.startswith('http'):
|
||||
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
|
||||
|
||||
params = {}
|
||||
if headphones.CONFIG.SAB_HOST.endswith('/'):
|
||||
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[
|
||||
0:len(headphones.CONFIG.SAB_HOST) - 1]
|
||||
|
||||
url = headphones.CONFIG.SAB_HOST + "/" + "api?"
|
||||
|
||||
if headphones.CONFIG.SAB_USERNAME:
|
||||
params['ma_username'] = headphones.CONFIG.SAB_USERNAME
|
||||
@@ -40,9 +40,24 @@ def sendNZB(nzb):
|
||||
params['ma_password'] = headphones.CONFIG.SAB_PASSWORD
|
||||
if headphones.CONFIG.SAB_APIKEY:
|
||||
params['apikey'] = headphones.CONFIG.SAB_APIKEY
|
||||
if headphones.CONFIG.SAB_CATEGORY:
|
||||
|
||||
if request_type == 'send_nzb' and headphones.CONFIG.SAB_CATEGORY:
|
||||
params['cat'] = headphones.CONFIG.SAB_CATEGORY
|
||||
|
||||
params['output'] = 'json'
|
||||
|
||||
response = request.request_json(url, params=params, **kwargs)
|
||||
|
||||
if not response:
|
||||
logger.error("Error connecting to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
|
||||
return False
|
||||
else:
|
||||
logger.debug("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
|
||||
return response
|
||||
|
||||
|
||||
def sendNZB(nzb):
|
||||
params = {}
|
||||
# if it's a normal result we just pass SAB the URL
|
||||
if nzb.resultType == "nzb":
|
||||
# for newzbin results send the ID to sab specifically
|
||||
@@ -62,102 +77,42 @@ def sendNZB(nzb):
|
||||
# Sanitize the file a bit, since we can only use ascii chars with MultiPartPostHandler
|
||||
nzbdata = helpers.latinToAscii(nzb.extraInfo[0])
|
||||
params['mode'] = 'addfile'
|
||||
multiPartParams = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)}
|
||||
files = {"nzbfile": (helpers.latinToAscii(nzb.name) + ".nzb", nzbdata)}
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
|
||||
if not headphones.CONFIG.SAB_HOST.startswith('http'):
|
||||
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
|
||||
logger.info("Attempting to connect to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
|
||||
if nzb.resultType == "nzb":
|
||||
response = sab_api_call('send_nzb', params=params)
|
||||
elif nzb.resultType == "nzbdata":
|
||||
cookies = cookielib.CookieJar()
|
||||
response = sab_api_call('send_nzb', params=params, method="post", files=files,
|
||||
cookies=cookies, headers=headers)
|
||||
|
||||
if headphones.CONFIG.SAB_HOST.endswith('/'):
|
||||
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1]
|
||||
|
||||
url = headphones.CONFIG.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
|
||||
|
||||
try:
|
||||
|
||||
if nzb.resultType == "nzb":
|
||||
f = urllib.urlopen(url)
|
||||
elif nzb.resultType == "nzbdata":
|
||||
cookies = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies),
|
||||
MultipartPostHandler.MultipartPostHandler)
|
||||
|
||||
req = urllib2.Request(url,
|
||||
multiPartParams,
|
||||
headers={'User-Agent': USER_AGENT})
|
||||
|
||||
f = opener.open(req)
|
||||
|
||||
except (EOFError, IOError) as e:
|
||||
logger.error(u"Unable to connect to SAB with URL: %s" % url)
|
||||
return False
|
||||
|
||||
except httplib.InvalidURL as e:
|
||||
logger.error(u"Invalid SAB host, check your config. Current host: %s" % headphones.CONFIG.SAB_HOST)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u"Error: " + str(e))
|
||||
return False
|
||||
|
||||
if f is None:
|
||||
if not response:
|
||||
logger.info(u"No data returned from SABnzbd, NZB not sent")
|
||||
return False
|
||||
|
||||
try:
|
||||
result = f.readlines()
|
||||
except Exception as e:
|
||||
logger.info(u"Error trying to get result from SAB, NZB not sent: ")
|
||||
return False
|
||||
|
||||
if len(result) == 0:
|
||||
logger.info(u"No data returned from SABnzbd, NZB not sent")
|
||||
return False
|
||||
|
||||
sabText = result[0].strip()
|
||||
|
||||
logger.info(u"Result text from SAB: " + sabText)
|
||||
|
||||
if sabText == "ok":
|
||||
logger.info(u"NZB sent to SAB successfully")
|
||||
if response['status']:
|
||||
logger.info(u"NZB sent to SABnzbd successfully")
|
||||
return True
|
||||
elif sabText == "Missing authentication":
|
||||
logger.info(u"Incorrect username/password sent to SAB, NZB not sent")
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Unknown failure sending NZB to sab. Return text is: " + sabText)
|
||||
logger.error(u"Error sending NZB to SABnzbd: %s" % response['error'])
|
||||
return False
|
||||
|
||||
|
||||
def checkConfig():
|
||||
|
||||
params = {'mode': 'get_config',
|
||||
'section': 'misc'
|
||||
}
|
||||
'section': 'misc',
|
||||
}
|
||||
|
||||
if headphones.CONFIG.SAB_USERNAME:
|
||||
params['ma_username'] = headphones.CONFIG.SAB_USERNAME
|
||||
if headphones.CONFIG.SAB_PASSWORD:
|
||||
params['ma_password'] = headphones.CONFIG.SAB_PASSWORD
|
||||
if headphones.CONFIG.SAB_APIKEY:
|
||||
params['apikey'] = headphones.CONFIG.SAB_APIKEY
|
||||
config_options = sab_api_call(params=params)
|
||||
|
||||
if not headphones.CONFIG.SAB_HOST.startswith('http'):
|
||||
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
|
||||
|
||||
if headphones.CONFIG.SAB_HOST.endswith('/'):
|
||||
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1]
|
||||
|
||||
url = headphones.CONFIG.SAB_HOST + "/" + "api?" + urllib.urlencode(params)
|
||||
|
||||
try:
|
||||
f = urllib.urlopen(url).read()
|
||||
except Exception:
|
||||
logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
|
||||
if not config_options:
|
||||
logger.warn(
|
||||
"Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
|
||||
return (0, 0)
|
||||
|
||||
config_options = ast.literal_eval(f)
|
||||
|
||||
replace_spaces = config_options['misc']['replace_spaces']
|
||||
replace_dots = config_options['misc']['replace_dots']
|
||||
replace_spaces = config_options['config']['misc']['replace_spaces']
|
||||
replace_dots = config_options['config']['misc']['replace_dots']
|
||||
|
||||
return (replace_spaces, replace_dots)
|
||||
|
||||
+737
-269
File diff suppressed because it is too large
Load Diff
@@ -1,349 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
# Headphones rutracker.org search
|
||||
# Functions called from searcher.py
|
||||
|
||||
from bencode import bencode as bencode, bdecode
|
||||
from urlparse import urlparse
|
||||
from bs4 import BeautifulSoup
|
||||
from tempfile import mkdtemp
|
||||
from hashlib import sha1
|
||||
|
||||
import headphones
|
||||
import requests
|
||||
import cookielib
|
||||
import urllib2
|
||||
import urllib
|
||||
import re
|
||||
import os
|
||||
|
||||
from headphones import db, logger
|
||||
|
||||
|
||||
class Rutracker():
|
||||
|
||||
logged_in = False
|
||||
|
||||
# Stores a number of login attempts to prevent recursion.
|
||||
#login_counter = 0
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.cookiejar = cookielib.CookieJar()
|
||||
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookiejar))
|
||||
urllib2.install_opener(self.opener)
|
||||
|
||||
def login(self, login, password):
|
||||
"""Implements tracker login procedure."""
|
||||
|
||||
self.logged_in = False
|
||||
|
||||
if login is None or password is None:
|
||||
return False
|
||||
|
||||
#self.login_counter += 1
|
||||
|
||||
# No recursion wanted.
|
||||
#if self.login_counter > 1:
|
||||
# return False
|
||||
|
||||
params = urllib.urlencode({"login_username": login,
|
||||
"login_password": password,
|
||||
"login": "Вход"})
|
||||
|
||||
try:
|
||||
self.opener.open("http://login.rutracker.org/forum/login.php", params)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check if we're logged in
|
||||
for cookie in self.cookiejar:
|
||||
if cookie.name == 'bb_data':
|
||||
self.logged_in = True
|
||||
|
||||
return self.logged_in
|
||||
|
||||
def searchurl(self, artist, album, year, format):
|
||||
"""
|
||||
Return the search url
|
||||
"""
|
||||
|
||||
# Build search url
|
||||
searchterm = ''
|
||||
if artist != 'Various Artists':
|
||||
searchterm = artist
|
||||
searchterm = searchterm + ' '
|
||||
searchterm = searchterm + album
|
||||
searchterm = searchterm + ' '
|
||||
searchterm = searchterm + year
|
||||
|
||||
providerurl = "http://rutracker.org/forum/tracker.php"
|
||||
|
||||
if format == 'lossless':
|
||||
format = '+lossless'
|
||||
elif format == 'lossless+mp3':
|
||||
format = '+lossless||mp3||aac'
|
||||
else:
|
||||
format = '+mp3||aac'
|
||||
|
||||
# sort by size, descending.
|
||||
sort = '&o=7&s=2'
|
||||
|
||||
searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort)
|
||||
|
||||
return searchurl
|
||||
|
||||
def search(self, searchurl, maxsize, minseeders, albumid):
|
||||
"""
|
||||
Parse the search results and return valid torrent list
|
||||
"""
|
||||
|
||||
titles = []
|
||||
urls = []
|
||||
seeders = []
|
||||
sizes = []
|
||||
torrentlist = []
|
||||
rulist = []
|
||||
|
||||
try:
|
||||
|
||||
page = self.opener.open(searchurl, timeout=60)
|
||||
soup = BeautifulSoup(page.read())
|
||||
|
||||
# Debug
|
||||
#logger.debug (soup.prettify())
|
||||
|
||||
# Title
|
||||
for link in soup.find_all('a', attrs={'class': 'med tLink hl-tags bold'}):
|
||||
title = link.get_text()
|
||||
titles.append(title)
|
||||
|
||||
# Download URL
|
||||
for link in soup.find_all('a', attrs={'class': 'small tr-dl dl-stub'}):
|
||||
url = link.get('href')
|
||||
urls.append(url)
|
||||
|
||||
# Seeders
|
||||
for link in soup.find_all('b', attrs={'class': 'seedmed'}):
|
||||
seeder = link.get_text()
|
||||
seeders.append(seeder)
|
||||
|
||||
# Size
|
||||
for link in soup.find_all('td', attrs={'class': 'row4 small nowrap tor-size'}):
|
||||
size = link.u.string
|
||||
sizes.append(size)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
# Combine lists
|
||||
torrentlist = zip(titles, urls, seeders, sizes)
|
||||
|
||||
# return if nothing found
|
||||
if not torrentlist:
|
||||
return False
|
||||
|
||||
# don't bother checking track counts anymore, let searcher filter instead
|
||||
# leave code in just in case
|
||||
check_track_count = False
|
||||
|
||||
if check_track_count:
|
||||
|
||||
# get headphones track count for album, return if not found
|
||||
myDB = db.DBConnection()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
hptrackcount = len(tracks)
|
||||
|
||||
if not hptrackcount:
|
||||
logger.info('headphones track info not found, cannot compare to torrent')
|
||||
return False
|
||||
|
||||
# Return all valid entries, ignored, required words now checked in searcher.py
|
||||
|
||||
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
|
||||
|
||||
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif']
|
||||
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive']
|
||||
|
||||
for torrent in torrentlist:
|
||||
|
||||
returntitle = torrent[0].encode('utf-8')
|
||||
url = torrent[1]
|
||||
seeders = torrent[2]
|
||||
size = torrent[3]
|
||||
|
||||
if int(size) <= maxsize and int(seeders) >= minseeders:
|
||||
|
||||
#Torrent topic page
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
|
||||
|
||||
# add to list
|
||||
if not check_track_count:
|
||||
valid = True
|
||||
else:
|
||||
|
||||
# Check torrent info
|
||||
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
|
||||
|
||||
# Debug
|
||||
#for cookie in self.cookiejar:
|
||||
# logger.debug ('Cookie: %s' % cookie)
|
||||
|
||||
try:
|
||||
page = self.opener.open(url)
|
||||
torrent = page.read()
|
||||
if torrent:
|
||||
decoded = bdecode(torrent)
|
||||
metainfo = decoded['info']
|
||||
page.close()
|
||||
except Exception as e:
|
||||
logger.error('Error getting torrent: %s' % e)
|
||||
return False
|
||||
|
||||
# get torrent track count and check for cue
|
||||
trackcount = 0
|
||||
cuecount = 0
|
||||
|
||||
if 'files' in metainfo: # multi
|
||||
for pathfile in metainfo['files']:
|
||||
path = pathfile['path']
|
||||
for file in path:
|
||||
if any(file.lower().endswith('.' + x.lower()) for x in formatlist):
|
||||
trackcount += 1
|
||||
if '.cue' in file:
|
||||
cuecount += 1
|
||||
|
||||
title = returntitle.lower()
|
||||
logger.debug('torrent title: %s' % title)
|
||||
logger.debug('headphones trackcount: %s' % hptrackcount)
|
||||
logger.debug('rutracker trackcount: %s' % trackcount)
|
||||
|
||||
# If torrent track count less than headphones track count, and there's a cue, then attempt to get track count from log(s)
|
||||
# This is for the case where we have a single .flac/.wav which can be split by cue
|
||||
# Not great, but shouldn't be doing this too often
|
||||
totallogcount = 0
|
||||
if trackcount < hptrackcount and cuecount > 0 and cuecount < hptrackcount:
|
||||
page = self.opener.open(topicurl, timeout=60)
|
||||
soup = BeautifulSoup(page.read())
|
||||
findtoc = soup.find_all(text='TOC of the extracted CD')
|
||||
if not findtoc:
|
||||
findtoc = soup.find_all(text='TOC извлечённого CD')
|
||||
for toc in findtoc:
|
||||
logcount = 0
|
||||
for toccontent in toc.find_all_next(text=True):
|
||||
cut_string = toccontent.split('|')
|
||||
new_string = cut_string[0].lstrip().rstrip()
|
||||
if new_string == '1' or new_string == '01':
|
||||
logcount = 1
|
||||
elif logcount > 0:
|
||||
if new_string.isdigit():
|
||||
logcount += 1
|
||||
else:
|
||||
break
|
||||
totallogcount = totallogcount + logcount
|
||||
|
||||
if totallogcount > 0:
|
||||
trackcount = totallogcount
|
||||
logger.debug('rutracker logtrackcount: %s' % totallogcount)
|
||||
|
||||
# If torrent track count = hp track count then return torrent,
|
||||
# if greater, check for deluxe/special/foreign editions
|
||||
# if less, then allow if it's a single track with a cue
|
||||
valid = False
|
||||
|
||||
if trackcount == hptrackcount:
|
||||
valid = True
|
||||
elif trackcount > hptrackcount:
|
||||
if any(deluxe in title for deluxe in deluxelist):
|
||||
valid = True
|
||||
|
||||
# Add to list
|
||||
if valid:
|
||||
rulist.append((returntitle, size, topicurl))
|
||||
else:
|
||||
if topicurl:
|
||||
logger.info(u'<a href="%s">Torrent</a> found with %s tracks but the selected headphones release has %s tracks, skipping for rutracker.org' % (topicurl, trackcount, hptrackcount))
|
||||
else:
|
||||
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (returntitle, int(size), int(seeders)))
|
||||
|
||||
return rulist
|
||||
|
||||
def get_torrent(self, url, savelocation=None):
|
||||
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
|
||||
downloadurl = 'http://dl.rutracker.org/forum/dl.php?t=' + torrent_id
|
||||
torrent_name = torrent_id + '.torrent'
|
||||
|
||||
try:
|
||||
prev = os.umask(headphones.UMASK)
|
||||
page = self.opener.open(downloadurl)
|
||||
torrent = page.read()
|
||||
decoded = bdecode(torrent)
|
||||
metainfo = decoded['info']
|
||||
tor_hash = sha1(bencode(metainfo)).hexdigest()
|
||||
if savelocation:
|
||||
download_path = os.path.join(savelocation, torrent_name)
|
||||
else:
|
||||
tempdir = mkdtemp(suffix='_rutracker_torrents')
|
||||
download_path = os.path.join(tempdir, torrent_name)
|
||||
|
||||
with open(download_path, 'wb') as f:
|
||||
f.write(torrent)
|
||||
os.umask(prev)
|
||||
|
||||
# Add file to utorrent
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
self.utorrent_add_file(download_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error getting torrent: %s', e)
|
||||
return False
|
||||
|
||||
return download_path, tor_hash
|
||||
|
||||
#TODO get this working in utorrent.py
|
||||
def utorrent_add_file(self, filename):
|
||||
|
||||
host = headphones.CONFIG.UTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
if host.endswith('/gui'):
|
||||
host = host[:-4]
|
||||
|
||||
base_url = host
|
||||
username = headphones.CONFIG.UTORRENT_USERNAME
|
||||
password = headphones.CONFIG.UTORRENT_PASSWORD
|
||||
|
||||
session = requests.Session()
|
||||
url = base_url + '/gui/'
|
||||
session.auth = (username, password)
|
||||
|
||||
try:
|
||||
r = session.get(url + 'token.html')
|
||||
except Exception:
|
||||
logger.exception('Error getting token')
|
||||
return
|
||||
|
||||
if r.status_code == '401':
|
||||
logger.debug('Error reaching utorrent')
|
||||
return
|
||||
|
||||
regex = re.search(r'.+>([^<]+)</div></html>', r.text)
|
||||
if regex is None:
|
||||
logger.debug('Error reading token')
|
||||
return
|
||||
|
||||
session.params = {'token': regex.group(1)}
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
try:
|
||||
session.post(url, params={'action': 'add-file'},
|
||||
files={'torrent_file': f})
|
||||
except Exception:
|
||||
logger.exception('Error adding file to utorrent')
|
||||
return
|
||||
@@ -0,0 +1,72 @@
|
||||
import os
|
||||
|
||||
from headphones.exceptions import SoftChrootError
|
||||
|
||||
|
||||
class SoftChroot(object):
|
||||
""" SoftChroot provides SOFT chrooting for UI
|
||||
|
||||
IMPORTANT: call methods of this class just in modules, which generates data for client UI. Try to avoid unnecessary usage.
|
||||
"""
|
||||
|
||||
enabled = False
|
||||
chroot = None
|
||||
|
||||
def __init__(self, path):
|
||||
if not path:
|
||||
# disabled
|
||||
return
|
||||
|
||||
path = path.strip()
|
||||
if not path:
|
||||
return
|
||||
|
||||
if (not os.path.exists(path) or
|
||||
not os.path.isdir(path)):
|
||||
raise SoftChrootError('No such directory: %s' % path)
|
||||
|
||||
path = path.rstrip(os.path.sep) + os.path.sep
|
||||
|
||||
self.enabled = True
|
||||
self.chroot = path
|
||||
|
||||
def isEnabled(self):
|
||||
return self.enabled
|
||||
|
||||
def getRoot(self):
|
||||
return self.chroot
|
||||
|
||||
def apply(self, path):
|
||||
if not self.enabled:
|
||||
return path
|
||||
|
||||
if not path:
|
||||
return path
|
||||
|
||||
p = path.strip()
|
||||
if not p:
|
||||
return path
|
||||
|
||||
if path.startswith(self.chroot):
|
||||
p = os.path.sep + path[len(self.chroot):]
|
||||
else:
|
||||
p = os.path.sep
|
||||
|
||||
return p
|
||||
|
||||
def revoke(self, path):
|
||||
if not self.enabled:
|
||||
return path
|
||||
|
||||
if not path:
|
||||
return path
|
||||
|
||||
p = path.strip()
|
||||
if not p:
|
||||
return path
|
||||
|
||||
if os.path.sep == p[0]:
|
||||
p = p[1:]
|
||||
|
||||
p = self.chroot + p
|
||||
return p
|
||||
@@ -0,0 +1,121 @@
|
||||
import os
|
||||
import mock
|
||||
from headphones.unittestcompat import TestCase, TestArgs
|
||||
|
||||
from headphones.softchroot import SoftChroot
|
||||
from headphones.exceptions import SoftChrootError
|
||||
|
||||
|
||||
class SoftChrootTest(TestCase):
|
||||
def test_create(self):
|
||||
""" create headphones.SoftChroot """
|
||||
|
||||
cf = SoftChroot('/tmp/')
|
||||
self.assertIsInstance(cf, SoftChroot)
|
||||
self.assertTrue(cf.isEnabled())
|
||||
self.assertEqual(cf.getRoot(), '/tmp/')
|
||||
|
||||
@TestArgs(
|
||||
(None),
|
||||
(''),
|
||||
(' '),
|
||||
)
|
||||
def test_create_disabled(self, empty_path):
|
||||
""" create DISABLED SoftChroot """
|
||||
|
||||
cf = SoftChroot(empty_path)
|
||||
self.assertIsInstance(cf, SoftChroot)
|
||||
self.assertFalse(cf.isEnabled())
|
||||
self.assertIsNone(cf.getRoot())
|
||||
|
||||
def test_create_on_not_exists_dir(self):
|
||||
""" create SoftChroot on non existent dir """
|
||||
|
||||
path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss')
|
||||
|
||||
cf = None
|
||||
with self.assertRaises(SoftChrootError) as exc:
|
||||
cf = SoftChroot(path)
|
||||
self.assertIsNone(cf)
|
||||
|
||||
self.assertRegexpMatches(str(exc.exception), r'No such directory')
|
||||
self.assertRegexpMatches(str(exc.exception), path)
|
||||
|
||||
@mock.patch('headphones.softchroot.os', wrap=os, name='OsMock')
|
||||
def test_create_on_file(self, os_mock):
|
||||
""" create SoftChroot on file, not a directory """
|
||||
|
||||
path = os.path.join('/tmp', 'notexist', 'asdf', '11', '12', 'np', 'itsssss')
|
||||
|
||||
os_mock.path.sep = os.path.sep
|
||||
os_mock.path.isdir.side_effect = lambda x: x != path
|
||||
|
||||
cf = None
|
||||
with self.assertRaises(SoftChrootError) as exc:
|
||||
cf = SoftChroot(path)
|
||||
self.assertIsNone(cf)
|
||||
|
||||
self.assertTrue(os_mock.path.isdir.called)
|
||||
|
||||
self.assertRegexpMatches(str(exc.exception), r'No such directory')
|
||||
self.assertRegexpMatches(str(exc.exception), path)
|
||||
|
||||
@TestArgs(
|
||||
(None, None),
|
||||
('', ''),
|
||||
(' ', ' '),
|
||||
('/tmp/', '/'),
|
||||
('/tmp/asdf', '/asdf'),
|
||||
)
|
||||
def test_apply(self, p, e):
|
||||
""" apply SoftChroot """
|
||||
sc = SoftChroot('/tmp/')
|
||||
a = sc.apply(p)
|
||||
self.assertEqual(a, e)
|
||||
|
||||
@TestArgs(
|
||||
('/'),
|
||||
('/nonch/path/asdf'),
|
||||
('tmp/asdf'),
|
||||
)
|
||||
def test_apply_out_of_root(self, p):
|
||||
""" apply SoftChroot to paths outside of the chroot """
|
||||
sc = SoftChroot('/tmp/')
|
||||
a = sc.apply(p)
|
||||
self.assertEqual(a, '/')
|
||||
|
||||
@TestArgs(
|
||||
(None, None),
|
||||
('', ''),
|
||||
(' ', ' '),
|
||||
('/', '/tmp/'),
|
||||
('/asdf', '/tmp/asdf'),
|
||||
('/asdf/', '/tmp/asdf/'),
|
||||
('localdir/adf', '/tmp/localdir/adf'),
|
||||
('localdir/adf/', '/tmp/localdir/adf/'),
|
||||
)
|
||||
def test_revoke(self, p, e):
|
||||
""" revoke SoftChroot """
|
||||
sc = SoftChroot('/tmp/')
|
||||
a = sc.revoke(p)
|
||||
self.assertEqual(a, e)
|
||||
|
||||
@TestArgs(
|
||||
(None),
|
||||
(''),
|
||||
(' '),
|
||||
('/tmp'),
|
||||
('/tmp/'),
|
||||
('/tmp/asdf'),
|
||||
('/tmp/localdir/adf'),
|
||||
('localdir/adf'),
|
||||
('localdir/adf/'),
|
||||
)
|
||||
def test_actions_on_disabled(self, p):
|
||||
""" disabled SoftChroot should not change args on apply and revoke """
|
||||
sc = SoftChroot(None)
|
||||
a = sc.apply(p)
|
||||
self.assertEqual(a, p)
|
||||
|
||||
r = sc.revoke(p)
|
||||
self.assertEqual(r, p)
|
||||
@@ -13,13 +13,14 @@
|
||||
# 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 db, utorrent, transmission, logger
|
||||
|
||||
import threading
|
||||
|
||||
from headphones import db, utorrent, transmission, logger
|
||||
import headphones
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
|
||||
|
||||
def checkTorrentFinished():
|
||||
"""
|
||||
Remove Torrent + data if Post Processed and finished Seeding
|
||||
@@ -41,6 +42,7 @@ def checkTorrentFinished():
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?', [albumid])
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
||||
[albumid])
|
||||
|
||||
logger.info("Checking finished torrents completed")
|
||||
|
||||
+45
-49
@@ -13,27 +13,32 @@
|
||||
# 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 logger, request
|
||||
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import urlparse
|
||||
|
||||
from headphones import logger, request
|
||||
import headphones
|
||||
|
||||
|
||||
# This is just a simple script to send torrents to transmission. The
|
||||
# intention is to turn this into a class where we can check the state
|
||||
# of the download, set the download dir, etc.
|
||||
# TODO: Store the session id so we don't need to make 2 calls
|
||||
# Store torrent id so we can check up on it
|
||||
# TODO: Store torrent id so we can check up on it
|
||||
|
||||
_session_id = None
|
||||
|
||||
|
||||
def addTorrent(link):
|
||||
def addTorrent(link, data=None):
|
||||
method = 'torrent-add'
|
||||
|
||||
if link.endswith('.torrent'):
|
||||
with open(link, 'rb') as f:
|
||||
metainfo = str(base64.b64encode(f.read()))
|
||||
if link.endswith('.torrent') and not link.startswith('http') or data:
|
||||
if data:
|
||||
metainfo = str(base64.b64encode(data))
|
||||
else:
|
||||
with open(link, 'rb') as f:
|
||||
metainfo = str(base64.b64encode(f.read()))
|
||||
arguments = {'metainfo': metainfo, 'download-dir': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||
else:
|
||||
arguments = {'filename': link, 'download-dir': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||
@@ -93,7 +98,6 @@ def setSeedRatio(torrentid, ratio):
|
||||
|
||||
|
||||
def removeTorrent(torrentid, remove_data=False):
|
||||
|
||||
method = 'torrent-get'
|
||||
arguments = {'ids': torrentid, 'fields': ['isFinished', 'name']}
|
||||
|
||||
@@ -115,7 +119,8 @@ def removeTorrent(torrentid, remove_data=False):
|
||||
response = torrentAction(method, arguments)
|
||||
return True
|
||||
else:
|
||||
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % name)
|
||||
logger.info(
|
||||
'%s has not finished seeding yet, torrent will not be removed, will try again on next run' % name)
|
||||
except:
|
||||
return False
|
||||
|
||||
@@ -123,7 +128,7 @@ def removeTorrent(torrentid, remove_data=False):
|
||||
|
||||
|
||||
def torrentAction(method, arguments):
|
||||
|
||||
global _session_id
|
||||
host = headphones.CONFIG.TRANSMISSION_HOST
|
||||
username = headphones.CONFIG.TRANSMISSION_USERNAME
|
||||
password = headphones.CONFIG.TRANSMISSION_PASSWORD
|
||||
@@ -145,43 +150,34 @@ def torrentAction(method, arguments):
|
||||
parts[2] += "/transmission/rpc"
|
||||
|
||||
host = urlparse.urlunparse(parts)
|
||||
|
||||
# Retrieve session id
|
||||
auth = (username, password) if username and password else None
|
||||
response = request.request_response(host, auth=auth,
|
||||
whitelist_status_code=[401, 409])
|
||||
|
||||
if response is None:
|
||||
logger.error("Error gettings Transmission session ID")
|
||||
return
|
||||
|
||||
# Parse response
|
||||
if response.status_code == 401:
|
||||
if auth:
|
||||
logger.error("Username and/or password not accepted by " \
|
||||
"Transmission")
|
||||
else:
|
||||
logger.error("Transmission authorization required")
|
||||
|
||||
return
|
||||
elif response.status_code == 409:
|
||||
session_id = response.headers['x-transmission-session-id']
|
||||
|
||||
if not session_id:
|
||||
logger.error("Expected a Session ID from Transmission")
|
||||
return
|
||||
|
||||
# Prepare next request
|
||||
headers = {'x-transmission-session-id': session_id}
|
||||
data = {'method': method, 'arguments': arguments}
|
||||
data_json = json.dumps(data)
|
||||
auth = (username, password) if username and password else None
|
||||
for retry in range(2):
|
||||
if _session_id is not None:
|
||||
headers = {'x-transmission-session-id': _session_id}
|
||||
response = request.request_response(host, method="POST",
|
||||
data=data_json, headers=headers, auth=auth,
|
||||
whitelist_status_code=[200, 401, 409])
|
||||
else:
|
||||
response = request.request_response(host, auth=auth,
|
||||
whitelist_status_code=[401, 409])
|
||||
if response.status_code == 401:
|
||||
if auth:
|
||||
logger.error("Username and/or password not accepted by "
|
||||
"Transmission")
|
||||
else:
|
||||
logger.error("Transmission authorization required")
|
||||
return
|
||||
elif response.status_code == 409:
|
||||
_session_id = response.headers['x-transmission-session-id']
|
||||
if _session_id is None:
|
||||
logger.error("Expected a Session ID from Transmission, got None")
|
||||
return
|
||||
# retry request with new session id
|
||||
logger.debug("Retrying Transmission request with new session id")
|
||||
continue
|
||||
|
||||
response = request.request_json(host, method="POST", data=json.dumps(data),
|
||||
headers=headers, auth=auth)
|
||||
|
||||
print response
|
||||
|
||||
if not response:
|
||||
logger.error("Error sending torrent to Transmission")
|
||||
return
|
||||
|
||||
return response
|
||||
resp_json = response.json()
|
||||
print resp_json
|
||||
return resp_json
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import sys
|
||||
if sys.version_info < (2, 7):
|
||||
import unittest2 as unittest
|
||||
from unittest2 import TestCase as TC
|
||||
else:
|
||||
import unittest
|
||||
from unittest import TestCase as TC
|
||||
|
||||
skip = unittest.skip
|
||||
|
||||
_dummy = False
|
||||
|
||||
# less than 2.6 ...
|
||||
if sys.version_info[0] == 2 and sys.version_info[1] <= 6:
|
||||
_dummy = True
|
||||
|
||||
|
||||
def _d(f):
|
||||
def decorate(self, *args, **kw):
|
||||
if not _dummy:
|
||||
return f(self, *args, **kw)
|
||||
return self.assertTrue(True)
|
||||
return decorate
|
||||
|
||||
|
||||
class TestCase(TC):
|
||||
"""
|
||||
Wrapper for python 2.6 stubs
|
||||
"""
|
||||
|
||||
def assertIsInstance(self, obj, cls, msg=None):
|
||||
if not _dummy:
|
||||
return super(TestCase, self).assertIsInstance(obj, cls, msg)
|
||||
tst = isinstance(obj, cls)
|
||||
return self.assertTrue(tst, msg)
|
||||
|
||||
@_d
|
||||
def assertNotIsInstance(self, *args, **kw):
|
||||
return super(TestCase, self).assertNotIsInstance(*args, **kw)
|
||||
|
||||
@_d
|
||||
def assertIn(self, *args, **kw):
|
||||
return super(TestCase, self).assertIn(*args, **kw)
|
||||
|
||||
@_d
|
||||
def assertRegexpMatches(self, *args, **kw):
|
||||
return super(TestCase, self).assertRegexpMatches(*args, **kw)
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# NOT DUMMY ASSERTIONS
|
||||
# -----------------------------------------------------------
|
||||
def assertIsNone(self, val, msg=None):
|
||||
if not _dummy:
|
||||
return super(TestCase, self).assertIsNone(val, msg)
|
||||
tst = val is None
|
||||
return super(TestCase, self).assertTrue(tst, msg)
|
||||
|
||||
def assertIsNotNone(self, val, msg=None):
|
||||
if not _dummy:
|
||||
return super(TestCase, self).assertIsNotNone(val, msg)
|
||||
tst = val is not None
|
||||
return super(TestCase, self).assertTrue(tst, msg)
|
||||
|
||||
def assertRaises(self, exc, msg=None):
|
||||
if not _dummy:
|
||||
return super(TestCase, self).assertRaises(exc, msg)
|
||||
return TestCase._TestCaseRaiseStub(self, exc, msg=msg)
|
||||
|
||||
def assertRaisesRegexp(self, exc, regex, msg=None):
|
||||
if not _dummy:
|
||||
return super(TestCase, self).assertRaises(exc, msg)
|
||||
return TestCase._TestCaseRaiseStub(self, exc, regex=regex, msg=msg)
|
||||
|
||||
class _TestCaseRaiseStub:
|
||||
""" Internal stuff for stubbing `assertRaises*` """
|
||||
|
||||
def __init__(self, test_case, exc, regex=None, msg=None):
|
||||
self.exc = exc
|
||||
self.test_case = test_case
|
||||
self.regex = regex
|
||||
self.msg = msg
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, tp, value, traceback):
|
||||
tst = tp is self.exc
|
||||
self.test_case.assertTrue(tst, msg=self.msg)
|
||||
self.exception = value
|
||||
|
||||
# TODO: implement self.regex checking
|
||||
|
||||
# True indicates, that exception is handled
|
||||
return True
|
||||
|
||||
|
||||
def TestArgs(*parameters):
|
||||
def tuplify(x):
|
||||
if not isinstance(x, tuple):
|
||||
return (x,)
|
||||
return x
|
||||
|
||||
def decorator(method, parameters=parameters):
|
||||
for parameter in (tuplify(x) for x in parameters):
|
||||
|
||||
def method_for_parameter(self, method=method, parameter=parameter):
|
||||
method(self, *parameter)
|
||||
args_for_parameter = ",".join(repr(v) for v in parameter)
|
||||
name_for_parameter = method.__name__ + "(" + args_for_parameter + ")"
|
||||
frame = sys._getframe(1) # pylint: disable-msg=W0212
|
||||
frame.f_locals[name_for_parameter] = method_for_parameter
|
||||
frame.f_locals[name_for_parameter].__doc__ = method.__doc__ + '(' + args_for_parameter + ')'
|
||||
method_for_parameter.__name__ = name_for_parameter + '(' + args_for_parameter + ')'
|
||||
return None
|
||||
return decorator
|
||||
@@ -17,10 +17,10 @@ from headphones import logger, db, importer
|
||||
|
||||
|
||||
def dbUpdate(forcefull=False):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
active_artists = myDB.select('SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC')
|
||||
active_artists = myDB.select(
|
||||
'SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC')
|
||||
logger.info('Starting update for %i active artists', len(active_artists))
|
||||
|
||||
for artist in active_artists:
|
||||
|
||||
+24
-19
@@ -14,26 +14,24 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import urllib
|
||||
import json
|
||||
import time
|
||||
from collections import namedtuple
|
||||
import urllib2
|
||||
import urlparse
|
||||
import cookielib
|
||||
import json
|
||||
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class utorrentclient(object):
|
||||
|
||||
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
|
||||
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
|
||||
|
||||
def __init__(self, base_url=None, username=None, password=None,):
|
||||
def __init__(self, base_url=None, username=None, password=None, ):
|
||||
|
||||
host = headphones.CONFIG.UTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
@@ -50,7 +48,7 @@ class utorrentclient(object):
|
||||
self.password = headphones.CONFIG.UTORRENT_PASSWORD
|
||||
self.opener = self._make_opener('uTorrent', self.base_url, self.username, self.password)
|
||||
self.token = self._get_token()
|
||||
#TODO refresh token, when necessary
|
||||
# TODO refresh token, when necessary
|
||||
|
||||
def _make_opener(self, realm, base_url, username, password):
|
||||
"""uTorrent API need HTTP Basic Auth and cookie support for token verify."""
|
||||
@@ -73,6 +71,7 @@ class utorrentclient(object):
|
||||
except urllib2.HTTPError as err:
|
||||
logger.debug('URL: ' + str(url))
|
||||
logger.debug('Error getting Token. uTorrent responded with error: ' + str(err))
|
||||
return
|
||||
match = re.search(utorrentclient.TOKEN_REGEX, response.read())
|
||||
return match.group(1)
|
||||
|
||||
@@ -82,7 +81,7 @@ class utorrentclient(object):
|
||||
return self._action(params)
|
||||
|
||||
def add_url(self, url):
|
||||
#can receive magnet or normal .torrent link
|
||||
# can receive magnet or normal .torrent link
|
||||
params = [('action', 'add-url'), ('s', url)]
|
||||
return self._action(params)
|
||||
|
||||
@@ -147,6 +146,10 @@ class utorrentclient(object):
|
||||
return self._action(params)
|
||||
|
||||
def _action(self, params, body=None, content_type=None):
|
||||
|
||||
if not self.token:
|
||||
return
|
||||
|
||||
url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params)
|
||||
request = urllib2.Request(url)
|
||||
|
||||
@@ -176,13 +179,15 @@ def removeTorrent(hash, remove_data=False):
|
||||
status, torrentList = uTorrentClient.list()
|
||||
torrents = torrentList['torrents']
|
||||
for torrent in torrents:
|
||||
if torrent[0].lower() == hash:
|
||||
if torrent[0].upper() == hash.upper():
|
||||
if torrent[21] == 'Finished':
|
||||
logger.info('%s has finished seeding, removing torrent and data' % torrent[2])
|
||||
uTorrentClient.remove(hash, remove_data)
|
||||
return True
|
||||
else:
|
||||
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent[2])
|
||||
logger.info(
|
||||
'%s has not finished seeding yet, torrent will not be removed, will try again on next run' %
|
||||
torrent[2])
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -198,7 +203,6 @@ def setSeedRatio(hash, ratio):
|
||||
|
||||
|
||||
def dirTorrent(hash, cacheid=None, return_name=None):
|
||||
|
||||
uTorrentClient = utorrentclient()
|
||||
|
||||
if not cacheid:
|
||||
@@ -215,7 +219,7 @@ def dirTorrent(hash, cacheid=None, return_name=None):
|
||||
cacheid = torrentList['torrentc']
|
||||
|
||||
for torrent in torrents:
|
||||
if torrent[0].upper() == hash:
|
||||
if torrent[0].upper() == hash.upper():
|
||||
if not return_name:
|
||||
return torrent[26], cacheid
|
||||
else:
|
||||
@@ -224,18 +228,21 @@ def dirTorrent(hash, cacheid=None, return_name=None):
|
||||
return None, None
|
||||
|
||||
|
||||
def addTorrent(link, hash):
|
||||
def addTorrent(link):
|
||||
uTorrentClient = utorrentclient()
|
||||
uTorrentClient.add_url(link)
|
||||
|
||||
|
||||
def getFolder(hash):
|
||||
|
||||
# Get Active Directory from settings
|
||||
active_dir, completed_dir = getSettingsDirectories()
|
||||
|
||||
if not active_dir:
|
||||
logger.error('Could not get "Put new downloads in:" directory from uTorrent settings, please ensure it is set')
|
||||
logger.error(
|
||||
'Could not get "Put new downloads in:" directory from uTorrent settings, please ensure it is set')
|
||||
return None
|
||||
|
||||
uTorrentClient.add_url(link)
|
||||
|
||||
# Get Torrent Folder Name
|
||||
torrent_folder, cacheid = dirTorrent(hash)
|
||||
|
||||
@@ -249,10 +256,8 @@ def addTorrent(link, hash):
|
||||
|
||||
if torrent_folder == active_dir or not torrent_folder:
|
||||
torrent_folder, cacheid = dirTorrent(hash, cacheid, return_name=True)
|
||||
labelTorrent(hash)
|
||||
return torrent_folder
|
||||
else:
|
||||
labelTorrent(hash)
|
||||
if headphones.SYS_PLATFORM != "win32":
|
||||
torrent_folder = torrent_folder.replace('\\', '/')
|
||||
return os.path.basename(os.path.normpath(torrent_folder))
|
||||
|
||||
+23
-15
@@ -13,18 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import os
|
||||
import tarfile
|
||||
import platform
|
||||
import headphones
|
||||
import subprocess
|
||||
|
||||
import re
|
||||
import os
|
||||
import headphones
|
||||
from headphones import logger, version, request
|
||||
|
||||
|
||||
def runGit(args):
|
||||
|
||||
if headphones.CONFIG.GIT_PATH:
|
||||
git_locations = ['"' + headphones.CONFIG.GIT_PATH + '"']
|
||||
else:
|
||||
@@ -40,7 +39,8 @@ def runGit(args):
|
||||
|
||||
try:
|
||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True,
|
||||
cwd=headphones.PROG_DIR)
|
||||
output, err = p.communicate()
|
||||
output = output.strip()
|
||||
|
||||
@@ -62,7 +62,6 @@ def runGit(args):
|
||||
|
||||
|
||||
def getVersion():
|
||||
|
||||
if version.HEADPHONES_VERSION.startswith('win32build'):
|
||||
headphones.INSTALL_TYPE = 'win'
|
||||
|
||||
@@ -92,7 +91,8 @@ def getVersion():
|
||||
branch_name = branch_name
|
||||
|
||||
if not branch_name and headphones.CONFIG.GIT_BRANCH:
|
||||
logger.error('Could not retrieve branch name from git. Falling back to %s' % headphones.CONFIG.GIT_BRANCH)
|
||||
logger.error(
|
||||
'Could not retrieve branch name from git. Falling back to %s' % headphones.CONFIG.GIT_BRANCH)
|
||||
branch_name = headphones.CONFIG.GIT_BRANCH
|
||||
if not branch_name:
|
||||
logger.error('Could not retrieve branch name from git. Defaulting to master')
|
||||
@@ -123,11 +123,13 @@ def checkGithub():
|
||||
|
||||
# Get the latest version available from github
|
||||
logger.info('Retrieving latest version information from GitHub')
|
||||
url = 'https://api.github.com/repos/%s/headphones/commits/%s' % (headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
url = 'https://api.github.com/repos/%s/headphones/commits/%s' % (
|
||||
headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
|
||||
|
||||
if version is None:
|
||||
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
logger.warn(
|
||||
'Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
return headphones.CURRENT_VERSION
|
||||
|
||||
headphones.LATEST_VERSION = version['sha']
|
||||
@@ -135,7 +137,8 @@ def checkGithub():
|
||||
|
||||
# See how many commits behind we are
|
||||
if not headphones.CURRENT_VERSION:
|
||||
logger.info('You are running an unknown version of Headphones. Run the updater to identify your version')
|
||||
logger.info(
|
||||
'You are running an unknown version of Headphones. Run the updater to identify your version')
|
||||
return headphones.LATEST_VERSION
|
||||
|
||||
if headphones.LATEST_VERSION == headphones.CURRENT_VERSION:
|
||||
@@ -143,8 +146,10 @@ def checkGithub():
|
||||
return headphones.LATEST_VERSION
|
||||
|
||||
logger.info('Comparing currently installed version with latest GitHub version')
|
||||
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (
|
||||
headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404,
|
||||
validator=lambda x: type(x) == dict)
|
||||
|
||||
if commits is None:
|
||||
logger.warn('Could not get commits behind from GitHub.')
|
||||
@@ -158,7 +163,8 @@ def checkGithub():
|
||||
headphones.COMMITS_BEHIND = 0
|
||||
|
||||
if headphones.COMMITS_BEHIND > 0:
|
||||
logger.info('New version is available. You are %s commits behind' % headphones.COMMITS_BEHIND)
|
||||
logger.info(
|
||||
'New version is available. You are %s commits behind' % headphones.COMMITS_BEHIND)
|
||||
elif headphones.COMMITS_BEHIND == 0:
|
||||
logger.info('Headphones is up to date')
|
||||
|
||||
@@ -185,7 +191,8 @@ def update():
|
||||
logger.info('Output: ' + str(output))
|
||||
|
||||
else:
|
||||
tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % (headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % (
|
||||
headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
update_dir = os.path.join(headphones.PROG_DIR, 'update')
|
||||
version_path = os.path.join(headphones.PROG_DIR, 'version.txt')
|
||||
|
||||
@@ -214,7 +221,8 @@ def update():
|
||||
os.remove(tar_download_path)
|
||||
|
||||
# Find update dir name
|
||||
update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))]
|
||||
update_dir_contents = [x for x in os.listdir(update_dir) if
|
||||
os.path.isdir(os.path.join(update_dir, x))]
|
||||
if len(update_dir_contents) != 1:
|
||||
logger.error("Invalid update data, update failed: " + str(update_dir_contents))
|
||||
return
|
||||
|
||||
+488
-156
File diff suppressed because it is too large
Load Diff
+13
-11
@@ -13,18 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import os
|
||||
import cherrypy
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
from headphones.webserve import WebInterface
|
||||
from headphones.helpers import create_https_certificates
|
||||
|
||||
|
||||
def initialize(options):
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
enable_https = options['enable_https']
|
||||
https_cert = options['https_cert']
|
||||
@@ -33,15 +32,16 @@ def initialize(options):
|
||||
if enable_https:
|
||||
# If either the HTTPS certificate or key do not exist, try to make
|
||||
# self-signed ones.
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (
|
||||
https_key and os.path.exists(https_key)):
|
||||
if not create_https_certificates(https_cert, https_key):
|
||||
logger.warn("Unable to create certificate and key. Disabling " \
|
||||
"HTTPS")
|
||||
logger.warn("Unable to create certificate and key. Disabling "
|
||||
"HTTPS")
|
||||
enable_https = False
|
||||
|
||||
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
||||
"key.")
|
||||
logger.warn("Disabled HTTPS because of missing certificate and "
|
||||
"key.")
|
||||
enable_https = False
|
||||
|
||||
options_dict = {
|
||||
@@ -63,7 +63,7 @@ def initialize(options):
|
||||
protocol = "http"
|
||||
|
||||
logger.info("Starting Headphones web server on %s://%s:%d/", protocol,
|
||||
options['http_host'], options['http_port'])
|
||||
options['http_host'], options['http_port'])
|
||||
cherrypy.config.update(options_dict)
|
||||
|
||||
conf = {
|
||||
@@ -99,7 +99,8 @@ def initialize(options):
|
||||
}
|
||||
|
||||
if options['http_password']:
|
||||
logger.info("Web server authentication is enabled, username is '%s'", options['http_username'])
|
||||
logger.info("Web server authentication is enabled, username is '%s'",
|
||||
options['http_username'])
|
||||
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
@@ -118,7 +119,8 @@ def initialize(options):
|
||||
cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
|
||||
cherrypy.server.start()
|
||||
except IOError:
|
||||
sys.stderr.write('Failed to start on port: %i. Is something else running?\n' % (options['http_port']))
|
||||
sys.stderr.write(
|
||||
'Failed to start on port: %i. Is something else running?\n' % (options['http_port']))
|
||||
sys.exit(1)
|
||||
|
||||
cherrypy.server.wait()
|
||||
|
||||
@@ -27,17 +27,17 @@ rcvar=${name}_enable
|
||||
|
||||
load_rc_config ${name}
|
||||
|
||||
: ${headphones_enable:="NO"}
|
||||
: ${headphones_user:="_sabnzbd"}
|
||||
: ${headphones_dir:="/usr/local/headphones"}
|
||||
: ${headphones_chdir:="${headphones_dir}"}
|
||||
: ${headphones_pid:="${headphones_dir}/headphones.pid"}
|
||||
: ${headphones_conf:="${headphones_dir}/config.ini"}
|
||||
: "${headphones_enable:="NO"}"
|
||||
: "${headphones_user:="_sabnzbd"}"
|
||||
: "${headphones_dir:="/usr/local/headphones"}"
|
||||
: "${headphones_chdir:="${headphones_dir}"}"
|
||||
: "${headphones_pid:="${headphones_dir}/headphones.pid"}"
|
||||
: "${headphones_conf:="${headphones_dir}/config.ini"}"
|
||||
|
||||
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown Headphones.
|
||||
if [ -e "${headphones_conf}" ]; then
|
||||
HOST=`grep -A64 "\[General\]" "${headphones_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/'`
|
||||
PORT=`grep -A64 "\[General\]" "${headphones_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/'`
|
||||
HOST=$(grep -A64 "\[General\]" "${headphones_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/')
|
||||
PORT=$(grep -A64 "\[General\]" "${headphones_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/')
|
||||
fi
|
||||
|
||||
status_cmd="${name}_status"
|
||||
@@ -53,15 +53,15 @@ if [ ! -x "${WGET}" ]; then
|
||||
fi
|
||||
|
||||
# Ensure user is root when running this script.
|
||||
if [ `id -u` != "0" ]; then
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo "Oops, you should be root before running this!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_headphones_pid() {
|
||||
# Make sure the pid corresponds to the Headphones process.
|
||||
pid=`cat ${headphones_pid} 2>/dev/null`
|
||||
ps -p ${pid} | grep -q "python ${headphones_dir}/Headphones.py"
|
||||
pid=$(cat "${headphones_pid}" 2>/dev/null)
|
||||
pgrep -F "${headphones_pid}" -q "python ${headphones_dir}/Headphones.py"
|
||||
return $?
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ headphones_stop() {
|
||||
fi
|
||||
echo "Stopping $name"
|
||||
verify_headphones_pid
|
||||
${WGET} -O - -q --user=${SBUSR} --password=${SBPWD} "http://${HOST}:${PORT}/shutdown/" >/dev/null
|
||||
${WGET} -O - -q --user="${SBUSR}" --password="${SBPWD}" "http://${HOST}:${PORT}/shutdown/" >/dev/null
|
||||
|
||||
if [ -n "${pid}" ]; then
|
||||
wait_for_pids ${pid}
|
||||
wait_for_pids "${pid}"
|
||||
echo "Stopped $name"
|
||||
fi
|
||||
}
|
||||
|
||||
+23
-44
@@ -1,7 +1,8 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: headphones
|
||||
# REQUIRE: DAEMON sabnzbd
|
||||
# REQUIRE: DAEMON
|
||||
# BEFORE: LOGIN
|
||||
# KEYWORD: shutdown
|
||||
#
|
||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||
@@ -15,56 +16,34 @@
|
||||
# as root.
|
||||
# headphones_dir: Directory where Headphones lives.
|
||||
# Default: /usr/local/headphones
|
||||
# headphones_chdir: Change to this directory before running Headphones.
|
||||
# Default is same as headphones_dir.
|
||||
# headphones_pid: The name of the pidfile to create.
|
||||
# Default is headphones.pid in headphones_dir.
|
||||
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="headphones"
|
||||
rcvar=${name}_enable
|
||||
|
||||
: "${headphones_enable:="NO"}"
|
||||
: "${headphones_user:="_sabnzbd"}"
|
||||
: "${headphones_dir:="/usr/local/headphones"}"
|
||||
: "${headphones_conf:="/usr/local/headphones/config.ini"}"
|
||||
|
||||
command="${headphones_dir}/Headphones.py"
|
||||
command_interpreter="/usr/bin/python"
|
||||
pidfile="/var/run/headphones/headphones.pid"
|
||||
start_precmd="headphones_start_precmd"
|
||||
headphones_flags="--daemon --nolaunch --pidfile $pidfile --config $headphones_conf $headphones_flags"
|
||||
|
||||
headphones_start_precmd() {
|
||||
if [ $($ID -u) != 0 ]; then
|
||||
err 1 "Must be root."
|
||||
fi
|
||||
|
||||
if [ ! -d /var/run/headphones ]; then
|
||||
install -do $headphones_user /var/run/headphones
|
||||
fi
|
||||
}
|
||||
|
||||
load_rc_config ${name}
|
||||
|
||||
: ${headphones_enable:="NO"}
|
||||
: ${headphones_user:="_sabnzbd"}
|
||||
: ${headphones_dir:="/usr/local/headphones"}
|
||||
: ${headphones_chdir:="${headphones_dir}"}
|
||||
: ${headphones_pid:="${headphones_dir}/headphones.pid"}
|
||||
|
||||
status_cmd="${name}_status"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-f -p ${headphones_pid} python ${headphones_dir}/Headphones.py ${headphones_flags} --quiet --nolaunch"
|
||||
|
||||
# Ensure user is root when running this script.
|
||||
if [ `id -u` != "0" ]; then
|
||||
echo "Oops, you should be root before running this!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_headphones_pid() {
|
||||
# Make sure the pid corresponds to the Headphones process.
|
||||
pid=`cat ${headphones_pid} 2>/dev/null`
|
||||
ps -p ${pid} | grep -q "python ${headphones_dir}/Headphones.py"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Try to stop Headphones cleanly by calling shutdown over http.
|
||||
headphones_stop() {
|
||||
echo "Stopping $name"
|
||||
verify_headphones_pid
|
||||
if [ -n "${pid}" ]; then
|
||||
wait_for_pids ${pid}
|
||||
echo "Stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
headphones_status() {
|
||||
verify_headphones_pid && echo "$name is running as ${pid}" || echo "$name is not running"
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.headphones.headphones</string>
|
||||
<string>headphones</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<!-- Modify these two lines if you need to to reflect your python location and Headphones install location -->
|
||||
<string>/usr/bin/python</string>
|
||||
<string>/Applications/Headphones/headphones.py</string>
|
||||
<string>--quiet</string>
|
||||
<string>--daemon</string>
|
||||
<string>--nolaunch</string>
|
||||
<string>/Applications/Headphones/Headphones.py</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
|
||||
+19
-17
@@ -32,8 +32,8 @@
|
||||
## HP_PIDFILE= #$PID_FILE, the location of headphones.pid, the default is /var/run/headphones/headphones.pid
|
||||
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
|
||||
## HP_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for headphones, i.e. " --config=/home/headphones/config.ini"
|
||||
## SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
|
||||
## HP_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
|
||||
## HP_HOST= #$HOST_OPTS, host for the webserver, overrides value in config.ini
|
||||
##
|
||||
## EXAMPLE if want to run as different user
|
||||
## add HP_USER=username to /etc/default/headphones
|
||||
@@ -101,20 +101,22 @@ load_settings() {
|
||||
# Extra daemon option like: HP_OPTS=" --config=/home/headphones/config.ini"
|
||||
EXTRA_DAEMON_OPTS=${HP_OPTS-}
|
||||
|
||||
# Extra start-stop-daemon option like START_OPTS=" --group=users"
|
||||
EXTRA_SSD_OPTS=${SSD_OPTS-}
|
||||
|
||||
# Hardcoded port to run on, overrides config.ini settings
|
||||
[ -n "$HP_PORT" ] && {
|
||||
PORT_OPTS=" --port=${HP_PORT} "
|
||||
}
|
||||
|
||||
DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
|
||||
# Host config
|
||||
[ -n "$HP_HOST" ] && {
|
||||
HOST_OPTS=" --host=${HP_HOST} "
|
||||
}
|
||||
|
||||
DAEMON_OPTS=" Headphones.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS} ${HOST_OPTS} ${EXTRA_DAEMON_OPTS}"
|
||||
|
||||
SETTINGS_LOADED=TRUE
|
||||
fi
|
||||
|
||||
[ -x $DAEMON ] || {
|
||||
[ -x "$DAEMON" ] || {
|
||||
log_warning_msg "$DESC: Can't execute daemon, aborting. See $DAEMON";
|
||||
return 1;}
|
||||
|
||||
@@ -125,8 +127,8 @@ load_settings || exit 0
|
||||
|
||||
is_running () {
|
||||
# returns 1 when running, else 0.
|
||||
if [ -e $PID_FILE ]; then
|
||||
PID=`cat $PID_FILE`
|
||||
if [ -e "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
RET=$?
|
||||
[ $RET -gt 1 ] && exit 1 || return $RET
|
||||
@@ -136,28 +138,28 @@ is_running () {
|
||||
}
|
||||
|
||||
handle_pid () {
|
||||
PID_PATH=`dirname $PID_FILE`
|
||||
[ -d $PID_PATH ] || mkdir -p $PID_PATH && chown -R $RUN_AS $PID_PATH > /dev/null || {
|
||||
PID_PATH=$(dirname "$PID_FILE")
|
||||
[ -d "$PID_PATH" ] || mkdir -p "$PID_PATH" && chown -R "$RUN_AS" "$PID_PATH" > /dev/null || {
|
||||
log_warning_msg "$DESC: Could not create $PID_FILE, See $SETTINGS, aborting.";
|
||||
return 1;}
|
||||
|
||||
if [ -e $PID_FILE ]; then
|
||||
PID=`cat $PID_FILE`
|
||||
if ! kill -0 $PID > /dev/null 2>&1; then
|
||||
if [ -e "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ! kill -0 "$PID" > /dev/null 2>&1; then
|
||||
log_warning_msg "Removing stale $PID_FILE"
|
||||
rm $PID_FILE
|
||||
rm "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
handle_datadir () {
|
||||
[ -d $DATA_DIR ] || mkdir -p $DATA_DIR && chown -R $RUN_AS $DATA_DIR > /dev/null || {
|
||||
[ -d "$DATA_DIR" ] || mkdir -p "$DATA_DIR" && chown -R "$RUN_AS" "$DATA_DIR" > /dev/null || {
|
||||
log_warning_msg "$DESC: Could not create $DATA_DIR, See $SETTINGS, aborting.";
|
||||
return 1;}
|
||||
}
|
||||
|
||||
handle_updates () {
|
||||
chown -R $RUN_AS $APP_PATH > /dev/null || {
|
||||
chown -R "$RUN_AS" "$APP_PATH" > /dev/null || {
|
||||
log_warning_msg "$DESC: $APP_PATH not writable by $RUN_AS for web-updates";
|
||||
return 0; }
|
||||
}
|
||||
@@ -178,7 +180,7 @@ start_headphones () {
|
||||
stop_headphones () {
|
||||
if is_running; then
|
||||
log_daemon_msg "Stopping $DESC"
|
||||
start-stop-daemon -o --stop --pidfile $PID_FILE --retry 15
|
||||
start-stop-daemon -o --stop --pidfile "$PID_FILE" --retry 15
|
||||
check_retval
|
||||
else
|
||||
log_success_msg "$DESC: not running"
|
||||
|
||||
+77
-15
@@ -17,8 +17,8 @@ http://www.crummy.com/software/BeautifulSoup/bs4/doc/
|
||||
"""
|
||||
|
||||
__author__ = "Leonard Richardson (leonardr@segfault.org)"
|
||||
__version__ = "4.3.2"
|
||||
__copyright__ = "Copyright (c) 2004-2013 Leonard Richardson"
|
||||
__version__ = "4.4.0"
|
||||
__copyright__ = "Copyright (c) 2004-2015 Leonard Richardson"
|
||||
__license__ = "MIT"
|
||||
|
||||
__all__ = ['BeautifulSoup']
|
||||
@@ -45,7 +45,7 @@ from .element import (
|
||||
|
||||
# The very first thing we do is give a useful error if someone is
|
||||
# running this code under Python 3 without converting it.
|
||||
syntax_error = u'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work. You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
|
||||
'You are trying to run the Python 2 version of Beautiful Soup under Python 3. This will not work.'<>'You need to convert the code, either by installing it (`python setup.py install`) or by running 2to3 (`2to3 -w bs4`).'
|
||||
|
||||
class BeautifulSoup(Tag):
|
||||
"""
|
||||
@@ -77,8 +77,11 @@ class BeautifulSoup(Tag):
|
||||
|
||||
ASCII_SPACES = '\x20\x0a\x09\x0c\x0d'
|
||||
|
||||
NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nTo get rid of this warning, change this:\n\n BeautifulSoup([your markup])\n\nto this:\n\n BeautifulSoup([your markup], \"%(parser)s\")\n"
|
||||
|
||||
def __init__(self, markup="", features=None, builder=None,
|
||||
parse_only=None, from_encoding=None, **kwargs):
|
||||
parse_only=None, from_encoding=None, exclude_encodings=None,
|
||||
**kwargs):
|
||||
"""The Soup object is initialized as the 'root tag', and the
|
||||
provided markup (which can be a string or a file-like object)
|
||||
is fed into the underlying parser."""
|
||||
@@ -114,9 +117,9 @@ class BeautifulSoup(Tag):
|
||||
del kwargs['isHTML']
|
||||
warnings.warn(
|
||||
"BS4 does not respect the isHTML argument to the "
|
||||
"BeautifulSoup constructor. You can pass in features='html' "
|
||||
"or features='xml' to get a builder capable of handling "
|
||||
"one or the other.")
|
||||
"BeautifulSoup constructor. Suggest you use "
|
||||
"features='lxml' for HTML and features='lxml-xml' for "
|
||||
"XML.")
|
||||
|
||||
def deprecated_argument(old_name, new_name):
|
||||
if old_name in kwargs:
|
||||
@@ -140,6 +143,7 @@ class BeautifulSoup(Tag):
|
||||
"__init__() got an unexpected keyword argument '%s'" % arg)
|
||||
|
||||
if builder is None:
|
||||
original_features = features
|
||||
if isinstance(features, basestring):
|
||||
features = [features]
|
||||
if features is None or len(features) == 0:
|
||||
@@ -151,6 +155,16 @@ class BeautifulSoup(Tag):
|
||||
"requested: %s. Do you need to install a parser library?"
|
||||
% ",".join(features))
|
||||
builder = builder_class()
|
||||
if not (original_features == builder.NAME or
|
||||
original_features in builder.ALTERNATE_NAMES):
|
||||
if builder.is_xml:
|
||||
markup_type = "XML"
|
||||
else:
|
||||
markup_type = "HTML"
|
||||
warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % dict(
|
||||
parser=builder.NAME,
|
||||
markup_type=markup_type))
|
||||
|
||||
self.builder = builder
|
||||
self.is_xml = builder.is_xml
|
||||
self.builder.soup = self
|
||||
@@ -178,6 +192,8 @@ class BeautifulSoup(Tag):
|
||||
# system. Just let it go.
|
||||
pass
|
||||
if is_file:
|
||||
if isinstance(markup, unicode):
|
||||
markup = markup.encode("utf8")
|
||||
warnings.warn(
|
||||
'"%s" looks like a filename, not markup. You should probably open this file and pass the filehandle into Beautiful Soup.' % markup)
|
||||
if markup[:5] == "http:" or markup[:6] == "https:":
|
||||
@@ -185,12 +201,15 @@ class BeautifulSoup(Tag):
|
||||
# Python 3 otherwise.
|
||||
if ((isinstance(markup, bytes) and not b' ' in markup)
|
||||
or (isinstance(markup, unicode) and not u' ' in markup)):
|
||||
if isinstance(markup, unicode):
|
||||
markup = markup.encode("utf8")
|
||||
warnings.warn(
|
||||
'"%s" looks like a URL. Beautiful Soup is not an HTTP client. You should probably use an HTTP client to get the document behind the URL, and feed that document to Beautiful Soup.' % markup)
|
||||
|
||||
for (self.markup, self.original_encoding, self.declared_html_encoding,
|
||||
self.contains_replacement_characters) in (
|
||||
self.builder.prepare_markup(markup, from_encoding)):
|
||||
self.builder.prepare_markup(
|
||||
markup, from_encoding, exclude_encodings=exclude_encodings)):
|
||||
self.reset()
|
||||
try:
|
||||
self._feed()
|
||||
@@ -203,6 +222,16 @@ class BeautifulSoup(Tag):
|
||||
self.markup = None
|
||||
self.builder.soup = None
|
||||
|
||||
def __copy__(self):
|
||||
return type(self)(self.encode(), builder=self.builder)
|
||||
|
||||
def __getstate__(self):
|
||||
# Frequently a tree builder can't be pickled.
|
||||
d = dict(self.__dict__)
|
||||
if 'builder' in d and not self.builder.picklable:
|
||||
del d['builder']
|
||||
return d
|
||||
|
||||
def _feed(self):
|
||||
# Convert the document to Unicode.
|
||||
self.builder.reset()
|
||||
@@ -229,9 +258,7 @@ class BeautifulSoup(Tag):
|
||||
|
||||
def new_string(self, s, subclass=NavigableString):
|
||||
"""Create a new NavigableString associated with this soup."""
|
||||
navigable = subclass(s)
|
||||
navigable.setup()
|
||||
return navigable
|
||||
return subclass(s)
|
||||
|
||||
def insert_before(self, successor):
|
||||
raise NotImplementedError("BeautifulSoup objects don't support insert_before().")
|
||||
@@ -290,14 +317,49 @@ class BeautifulSoup(Tag):
|
||||
def object_was_parsed(self, o, parent=None, most_recent_element=None):
|
||||
"""Add an object to the parse tree."""
|
||||
parent = parent or self.currentTag
|
||||
most_recent_element = most_recent_element or self._most_recent_element
|
||||
o.setup(parent, most_recent_element)
|
||||
previous_element = most_recent_element or self._most_recent_element
|
||||
|
||||
next_element = previous_sibling = next_sibling = None
|
||||
if isinstance(o, Tag):
|
||||
next_element = o.next_element
|
||||
next_sibling = o.next_sibling
|
||||
previous_sibling = o.previous_sibling
|
||||
if not previous_element:
|
||||
previous_element = o.previous_element
|
||||
|
||||
o.setup(parent, previous_element, next_element, previous_sibling, next_sibling)
|
||||
|
||||
if most_recent_element is not None:
|
||||
most_recent_element.next_element = o
|
||||
self._most_recent_element = o
|
||||
parent.contents.append(o)
|
||||
|
||||
if parent.next_sibling:
|
||||
# This node is being inserted into an element that has
|
||||
# already been parsed. Deal with any dangling references.
|
||||
index = parent.contents.index(o)
|
||||
if index == 0:
|
||||
previous_element = parent
|
||||
previous_sibling = None
|
||||
else:
|
||||
previous_element = previous_sibling = parent.contents[index-1]
|
||||
if index == len(parent.contents)-1:
|
||||
next_element = parent.next_sibling
|
||||
next_sibling = None
|
||||
else:
|
||||
next_element = next_sibling = parent.contents[index+1]
|
||||
|
||||
o.previous_element = previous_element
|
||||
if previous_element:
|
||||
previous_element.next_element = o
|
||||
o.next_element = next_element
|
||||
if next_element:
|
||||
next_element.previous_element = o
|
||||
o.next_sibling = next_sibling
|
||||
if next_sibling:
|
||||
next_sibling.previous_sibling = o
|
||||
o.previous_sibling = previous_sibling
|
||||
if previous_sibling:
|
||||
previous_sibling.next_sibling = o
|
||||
|
||||
def _popToTag(self, name, nsprefix=None, inclusivePop=True):
|
||||
"""Pops the tag stack up to and including the most recent
|
||||
instance of the given tag. If inclusivePop is false, pops the tag
|
||||
|
||||
@@ -80,9 +80,12 @@ builder_registry = TreeBuilderRegistry()
|
||||
class TreeBuilder(object):
|
||||
"""Turn a document into a Beautiful Soup object tree."""
|
||||
|
||||
NAME = "[Unknown tree builder]"
|
||||
ALTERNATE_NAMES = []
|
||||
features = []
|
||||
|
||||
is_xml = False
|
||||
picklable = False
|
||||
preserve_whitespace_tags = set()
|
||||
empty_element_tags = None # A tag will be considered an empty-element
|
||||
# tag when and only when it has no contents.
|
||||
|
||||
@@ -2,6 +2,7 @@ __all__ = [
|
||||
'HTML5TreeBuilder',
|
||||
]
|
||||
|
||||
from pdb import set_trace
|
||||
import warnings
|
||||
from bs4.builder import (
|
||||
PERMISSIVE,
|
||||
@@ -9,7 +10,10 @@ from bs4.builder import (
|
||||
HTML_5,
|
||||
HTMLTreeBuilder,
|
||||
)
|
||||
from bs4.element import NamespacedAttribute
|
||||
from bs4.element import (
|
||||
NamespacedAttribute,
|
||||
whitespace_re,
|
||||
)
|
||||
import html5lib
|
||||
from html5lib.constants import namespaces
|
||||
from bs4.element import (
|
||||
@@ -22,11 +26,20 @@ from bs4.element import (
|
||||
class HTML5TreeBuilder(HTMLTreeBuilder):
|
||||
"""Use html5lib to build a tree."""
|
||||
|
||||
features = ['html5lib', PERMISSIVE, HTML_5, HTML]
|
||||
NAME = "html5lib"
|
||||
|
||||
def prepare_markup(self, markup, user_specified_encoding):
|
||||
features = [NAME, PERMISSIVE, HTML_5, HTML]
|
||||
|
||||
def prepare_markup(self, markup, user_specified_encoding,
|
||||
document_declared_encoding=None, exclude_encodings=None):
|
||||
# Store the user-specified encoding for use later on.
|
||||
self.user_specified_encoding = user_specified_encoding
|
||||
|
||||
# document_declared_encoding and exclude_encodings aren't used
|
||||
# ATM because the html5lib TreeBuilder doesn't use
|
||||
# UnicodeDammit.
|
||||
if exclude_encodings:
|
||||
warnings.warn("You provided a value for exclude_encoding, but the html5lib tree builder doesn't support exclude_encoding.")
|
||||
yield (markup, None, None, False)
|
||||
|
||||
# These methods are defined by Beautiful Soup.
|
||||
@@ -101,7 +114,13 @@ class AttrList(object):
|
||||
def __iter__(self):
|
||||
return list(self.attrs.items()).__iter__()
|
||||
def __setitem__(self, name, value):
|
||||
"set attr", name, value
|
||||
# If this attribute is a multi-valued attribute for this element,
|
||||
# turn its value into a list.
|
||||
list_attr = HTML5TreeBuilder.cdata_list_attributes
|
||||
if (name in list_attr['*']
|
||||
or (self.element.name in list_attr
|
||||
and name in list_attr[self.element.name])):
|
||||
value = whitespace_re.split(value)
|
||||
self.element[name] = value
|
||||
def items(self):
|
||||
return list(self.attrs.items())
|
||||
@@ -161,6 +180,12 @@ class Element(html5lib.treebuilders._base.Node):
|
||||
# immediately after the parent, if it has no children.)
|
||||
if self.element.contents:
|
||||
most_recent_element = self.element._last_descendant(False)
|
||||
elif self.element.next_element is not None:
|
||||
# Something from further ahead in the parse tree is
|
||||
# being inserted into this earlier element. This is
|
||||
# very annoying because it means an expensive search
|
||||
# for the last element in the tree.
|
||||
most_recent_element = self.soup._last_descendant()
|
||||
else:
|
||||
most_recent_element = self.element
|
||||
|
||||
@@ -172,6 +197,7 @@ class Element(html5lib.treebuilders._base.Node):
|
||||
return AttrList(self.element)
|
||||
|
||||
def setAttributes(self, attributes):
|
||||
|
||||
if attributes is not None and len(attributes) > 0:
|
||||
|
||||
converted_attributes = []
|
||||
@@ -218,6 +244,9 @@ class Element(html5lib.treebuilders._base.Node):
|
||||
|
||||
def reparentChildren(self, new_parent):
|
||||
"""Move all of this tag's children into another tag."""
|
||||
# print "MOVE", self.element.contents
|
||||
# print "FROM", self.element
|
||||
# print "TO", new_parent.element
|
||||
element = self.element
|
||||
new_parent_element = new_parent.element
|
||||
# Determine what this tag's next_element will be once all the children
|
||||
@@ -236,17 +265,28 @@ class Element(html5lib.treebuilders._base.Node):
|
||||
new_parents_last_descendant_next_element = new_parent_element.next_element
|
||||
|
||||
to_append = element.contents
|
||||
append_after = new_parent.element.contents
|
||||
append_after = new_parent_element.contents
|
||||
if len(to_append) > 0:
|
||||
# Set the first child's previous_element and previous_sibling
|
||||
# to elements within the new parent
|
||||
first_child = to_append[0]
|
||||
first_child.previous_element = new_parents_last_descendant
|
||||
if new_parents_last_descendant:
|
||||
first_child.previous_element = new_parents_last_descendant
|
||||
else:
|
||||
first_child.previous_element = new_parent_element
|
||||
first_child.previous_sibling = new_parents_last_child
|
||||
if new_parents_last_descendant:
|
||||
new_parents_last_descendant.next_element = first_child
|
||||
else:
|
||||
new_parent_element.next_element = first_child
|
||||
if new_parents_last_child:
|
||||
new_parents_last_child.next_sibling = first_child
|
||||
|
||||
# Fix the last child's next_element and next_sibling
|
||||
last_child = to_append[-1]
|
||||
last_child.next_element = new_parents_last_descendant_next_element
|
||||
if new_parents_last_descendant_next_element:
|
||||
new_parents_last_descendant_next_element.previous_element = last_child
|
||||
last_child.next_sibling = None
|
||||
|
||||
for child in to_append:
|
||||
@@ -257,6 +297,10 @@ class Element(html5lib.treebuilders._base.Node):
|
||||
element.contents = []
|
||||
element.next_element = final_next_element
|
||||
|
||||
# print "DONE WITH MOVE"
|
||||
# print "FROM", self.element
|
||||
# print "TO", new_parent_element
|
||||
|
||||
def cloneNode(self):
|
||||
tag = self.soup.new_tag(self.element.name, self.namespace)
|
||||
node = Element(tag, self.soup, self.namespace)
|
||||
@@ -268,7 +312,7 @@ class Element(html5lib.treebuilders._base.Node):
|
||||
return self.element.contents
|
||||
|
||||
def getNameTuple(self):
|
||||
if self.namespace is None:
|
||||
if self.namespace == None:
|
||||
return namespaces["html"], self.name
|
||||
else:
|
||||
return self.namespace, self.name
|
||||
|
||||
@@ -4,10 +4,16 @@ __all__ = [
|
||||
'HTMLParserTreeBuilder',
|
||||
]
|
||||
|
||||
from HTMLParser import (
|
||||
HTMLParser,
|
||||
HTMLParseError,
|
||||
)
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
try:
|
||||
from HTMLParser import HTMLParseError
|
||||
except ImportError, e:
|
||||
# HTMLParseError is removed in Python 3.5. Since it can never be
|
||||
# thrown in 3.5, we can just define our own class as a placeholder.
|
||||
class HTMLParseError(Exception):
|
||||
pass
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
@@ -19,10 +25,10 @@ import warnings
|
||||
# At the end of this file, we monkeypatch HTMLParser so that
|
||||
# strict=True works well on Python 3.2.2.
|
||||
major, minor, release = sys.version_info[:3]
|
||||
CONSTRUCTOR_TAKES_STRICT = (
|
||||
major > 3
|
||||
or (major == 3 and minor > 2)
|
||||
or (major == 3 and minor == 2 and release >= 3))
|
||||
CONSTRUCTOR_TAKES_STRICT = major == 3 and minor == 2 and release >= 3
|
||||
CONSTRUCTOR_STRICT_IS_DEPRECATED = major == 3 and minor == 3
|
||||
CONSTRUCTOR_TAKES_CONVERT_CHARREFS = major == 3 and minor >= 4
|
||||
|
||||
|
||||
from bs4.element import (
|
||||
CData,
|
||||
@@ -63,7 +69,8 @@ class BeautifulSoupHTMLParser(HTMLParser):
|
||||
|
||||
def handle_charref(self, name):
|
||||
# XXX workaround for a bug in HTMLParser. Remove this once
|
||||
# it's fixed.
|
||||
# it's fixed in all supported versions.
|
||||
# http://bugs.python.org/issue13633
|
||||
if name.startswith('x'):
|
||||
real_name = int(name.lstrip('x'), 16)
|
||||
elif name.startswith('X'):
|
||||
@@ -113,14 +120,6 @@ class BeautifulSoupHTMLParser(HTMLParser):
|
||||
|
||||
def handle_pi(self, data):
|
||||
self.soup.endData()
|
||||
if data.endswith("?") and data.lower().startswith("xml"):
|
||||
# "An XHTML processing instruction using the trailing '?'
|
||||
# will cause the '?' to be included in data." - HTMLParser
|
||||
# docs.
|
||||
#
|
||||
# Strip the question mark so we don't end up with two
|
||||
# question marks.
|
||||
data = data[:-1]
|
||||
self.soup.handle_data(data)
|
||||
self.soup.endData(ProcessingInstruction)
|
||||
|
||||
@@ -128,15 +127,19 @@ class BeautifulSoupHTMLParser(HTMLParser):
|
||||
class HTMLParserTreeBuilder(HTMLTreeBuilder):
|
||||
|
||||
is_xml = False
|
||||
features = [HTML, STRICT, HTMLPARSER]
|
||||
picklable = True
|
||||
NAME = HTMLPARSER
|
||||
features = [NAME, HTML, STRICT]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if CONSTRUCTOR_TAKES_STRICT:
|
||||
if CONSTRUCTOR_TAKES_STRICT and not CONSTRUCTOR_STRICT_IS_DEPRECATED:
|
||||
kwargs['strict'] = False
|
||||
if CONSTRUCTOR_TAKES_CONVERT_CHARREFS:
|
||||
kwargs['convert_charrefs'] = False
|
||||
self.parser_args = (args, kwargs)
|
||||
|
||||
def prepare_markup(self, markup, user_specified_encoding=None,
|
||||
document_declared_encoding=None):
|
||||
document_declared_encoding=None, exclude_encodings=None):
|
||||
"""
|
||||
:return: A 4-tuple (markup, original encoding, encoding
|
||||
declared within markup, whether any characters had to be
|
||||
@@ -147,7 +150,8 @@ class HTMLParserTreeBuilder(HTMLTreeBuilder):
|
||||
return
|
||||
|
||||
try_encodings = [user_specified_encoding, document_declared_encoding]
|
||||
dammit = UnicodeDammit(markup, try_encodings, is_html=True)
|
||||
dammit = UnicodeDammit(markup, try_encodings, is_html=True,
|
||||
exclude_encodings=exclude_encodings)
|
||||
yield (dammit.markup, dammit.original_encoding,
|
||||
dammit.declared_html_encoding,
|
||||
dammit.contains_replacement_characters)
|
||||
|
||||
@@ -7,7 +7,12 @@ from io import BytesIO
|
||||
from StringIO import StringIO
|
||||
import collections
|
||||
from lxml import etree
|
||||
from bs4.element import Comment, Doctype, NamespacedAttribute
|
||||
from bs4.element import (
|
||||
Comment,
|
||||
Doctype,
|
||||
NamespacedAttribute,
|
||||
ProcessingInstruction,
|
||||
)
|
||||
from bs4.builder import (
|
||||
FAST,
|
||||
HTML,
|
||||
@@ -25,8 +30,11 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||
|
||||
is_xml = True
|
||||
|
||||
NAME = "lxml-xml"
|
||||
ALTERNATE_NAMES = ["xml"]
|
||||
|
||||
# Well, it's permissive by XML parser standards.
|
||||
features = [LXML, XML, FAST, PERMISSIVE]
|
||||
features = [NAME, LXML, XML, FAST, PERMISSIVE]
|
||||
|
||||
CHUNK_SIZE = 512
|
||||
|
||||
@@ -70,6 +78,7 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||
return (None, tag)
|
||||
|
||||
def prepare_markup(self, markup, user_specified_encoding=None,
|
||||
exclude_encodings=None,
|
||||
document_declared_encoding=None):
|
||||
"""
|
||||
:yield: A series of 4-tuples.
|
||||
@@ -95,7 +104,8 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||
# the document as each one in turn.
|
||||
is_html = not self.is_xml
|
||||
try_encodings = [user_specified_encoding, document_declared_encoding]
|
||||
detector = EncodingDetector(markup, try_encodings, is_html)
|
||||
detector = EncodingDetector(
|
||||
markup, try_encodings, is_html, exclude_encodings)
|
||||
for encoding in detector.encodings:
|
||||
yield (detector.markup, encoding, document_declared_encoding, False)
|
||||
|
||||
@@ -189,7 +199,9 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||
self.nsmaps.pop()
|
||||
|
||||
def pi(self, target, data):
|
||||
pass
|
||||
self.soup.endData()
|
||||
self.soup.handle_data(target + ' ' + data)
|
||||
self.soup.endData(ProcessingInstruction)
|
||||
|
||||
def data(self, content):
|
||||
self.soup.handle_data(content)
|
||||
@@ -212,7 +224,10 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
||||
|
||||
class LXMLTreeBuilder(HTMLTreeBuilder, LXMLTreeBuilderForXML):
|
||||
|
||||
features = [LXML, HTML, FAST, PERMISSIVE]
|
||||
NAME = LXML
|
||||
ALTERNATE_NAMES = ["lxml-html"]
|
||||
|
||||
features = ALTERNATE_NAMES + [NAME, HTML, FAST, PERMISSIVE]
|
||||
is_xml = False
|
||||
|
||||
def default_parser(self, encoding):
|
||||
|
||||
+16
-6
@@ -3,10 +3,11 @@
|
||||
|
||||
This library converts a bytestream to Unicode through any means
|
||||
necessary. It is heavily based on code from Mark Pilgrim's Universal
|
||||
Feed Parser. It works best on XML and XML, but it does not rewrite the
|
||||
Feed Parser. It works best on XML and HTML, but it does not rewrite the
|
||||
XML or HTML to reflect a new encoding; that's the tree builder's job.
|
||||
"""
|
||||
|
||||
from pdb import set_trace
|
||||
import codecs
|
||||
from htmlentitydefs import codepoint2name
|
||||
import re
|
||||
@@ -212,8 +213,11 @@ class EncodingDetector:
|
||||
|
||||
5. Windows-1252.
|
||||
"""
|
||||
def __init__(self, markup, override_encodings=None, is_html=False):
|
||||
def __init__(self, markup, override_encodings=None, is_html=False,
|
||||
exclude_encodings=None):
|
||||
self.override_encodings = override_encodings or []
|
||||
exclude_encodings = exclude_encodings or []
|
||||
self.exclude_encodings = set([x.lower() for x in exclude_encodings])
|
||||
self.chardet_encoding = None
|
||||
self.is_html = is_html
|
||||
self.declared_encoding = None
|
||||
@@ -224,6 +228,8 @@ class EncodingDetector:
|
||||
def _usable(self, encoding, tried):
|
||||
if encoding is not None:
|
||||
encoding = encoding.lower()
|
||||
if encoding in self.exclude_encodings:
|
||||
return False
|
||||
if encoding not in tried:
|
||||
tried.add(encoding)
|
||||
return True
|
||||
@@ -266,6 +272,9 @@ class EncodingDetector:
|
||||
def strip_byte_order_mark(cls, data):
|
||||
"""If a byte-order mark is present, strip it and return the encoding it implies."""
|
||||
encoding = None
|
||||
if isinstance(data, unicode):
|
||||
# Unicode data cannot have a byte-order mark.
|
||||
return data, encoding
|
||||
if (len(data) >= 4) and (data[:2] == b'\xfe\xff') \
|
||||
and (data[2:4] != '\x00\x00'):
|
||||
encoding = 'utf-16be'
|
||||
@@ -299,14 +308,14 @@ class EncodingDetector:
|
||||
else:
|
||||
xml_endpos = 1024
|
||||
html_endpos = max(2048, int(len(markup) * 0.05))
|
||||
|
||||
|
||||
declared_encoding = None
|
||||
declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos)
|
||||
if not declared_encoding_match and is_html:
|
||||
declared_encoding_match = html_meta_re.search(markup, endpos=html_endpos)
|
||||
if declared_encoding_match is not None:
|
||||
declared_encoding = declared_encoding_match.groups()[0].decode(
|
||||
'ascii')
|
||||
'ascii', 'replace')
|
||||
if declared_encoding:
|
||||
return declared_encoding.lower()
|
||||
return None
|
||||
@@ -331,13 +340,14 @@ class UnicodeDammit:
|
||||
]
|
||||
|
||||
def __init__(self, markup, override_encodings=[],
|
||||
smart_quotes_to=None, is_html=False):
|
||||
smart_quotes_to=None, is_html=False, exclude_encodings=[]):
|
||||
self.smart_quotes_to = smart_quotes_to
|
||||
self.tried_encodings = []
|
||||
self.contains_replacement_characters = False
|
||||
self.is_html = is_html
|
||||
|
||||
self.detector = EncodingDetector(markup, override_encodings, is_html)
|
||||
self.detector = EncodingDetector(
|
||||
markup, override_encodings, is_html, exclude_encodings)
|
||||
|
||||
# Short-circuit if the data is in Unicode to begin with.
|
||||
if isinstance(markup, unicode) or markup == '':
|
||||
|
||||
+15
-6
@@ -33,12 +33,21 @@ def diagnose(data):
|
||||
|
||||
if 'lxml' in basic_parsers:
|
||||
basic_parsers.append(["lxml", "xml"])
|
||||
from lxml import etree
|
||||
print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION))
|
||||
try:
|
||||
from lxml import etree
|
||||
print "Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION))
|
||||
except ImportError, e:
|
||||
print (
|
||||
"lxml is not installed or couldn't be imported.")
|
||||
|
||||
|
||||
if 'html5lib' in basic_parsers:
|
||||
import html5lib
|
||||
print "Found html5lib version %s" % html5lib.__version__
|
||||
try:
|
||||
import html5lib
|
||||
print "Found html5lib version %s" % html5lib.__version__
|
||||
except ImportError, e:
|
||||
print (
|
||||
"html5lib is not installed or couldn't be imported.")
|
||||
|
||||
if hasattr(data, 'read'):
|
||||
data = data.read()
|
||||
@@ -135,7 +144,7 @@ def rword(length=5):
|
||||
def rsentence(length=4):
|
||||
"Generate a random sentence-like string."
|
||||
return " ".join(rword(random.randint(4,9)) for i in range(length))
|
||||
|
||||
|
||||
def rdoc(num_elements=1000):
|
||||
"""Randomly generate an invalid HTML document."""
|
||||
tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table']
|
||||
@@ -159,7 +168,7 @@ def benchmark_parsers(num_elements=100000):
|
||||
print "Comparative parser benchmark on Beautiful Soup %s" % __version__
|
||||
data = rdoc(num_elements)
|
||||
print "Generated a large invalid HTML document (%d bytes)." % len(data)
|
||||
|
||||
|
||||
for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]:
|
||||
success = False
|
||||
try:
|
||||
|
||||
+271
-169
@@ -1,3 +1,4 @@
|
||||
from pdb import set_trace
|
||||
import collections
|
||||
import re
|
||||
import sys
|
||||
@@ -185,24 +186,40 @@ class PageElement(object):
|
||||
return self.HTML_FORMATTERS.get(
|
||||
name, HTMLAwareEntitySubstitution.substitute_xml)
|
||||
|
||||
def setup(self, parent=None, previous_element=None):
|
||||
def setup(self, parent=None, previous_element=None, next_element=None,
|
||||
previous_sibling=None, next_sibling=None):
|
||||
"""Sets up the initial relations between this element and
|
||||
other elements."""
|
||||
self.parent = parent
|
||||
|
||||
self.previous_element = previous_element
|
||||
if previous_element is not None:
|
||||
self.previous_element.next_element = self
|
||||
self.next_element = None
|
||||
self.previous_sibling = None
|
||||
self.next_sibling = None
|
||||
if self.parent is not None and self.parent.contents:
|
||||
self.previous_sibling = self.parent.contents[-1]
|
||||
|
||||
self.next_element = next_element
|
||||
if self.next_element:
|
||||
self.next_element.previous_element = self
|
||||
|
||||
self.next_sibling = next_sibling
|
||||
if self.next_sibling:
|
||||
self.next_sibling.previous_sibling = self
|
||||
|
||||
if (not previous_sibling
|
||||
and self.parent is not None and self.parent.contents):
|
||||
previous_sibling = self.parent.contents[-1]
|
||||
|
||||
self.previous_sibling = previous_sibling
|
||||
if previous_sibling:
|
||||
self.previous_sibling.next_sibling = self
|
||||
|
||||
nextSibling = _alias("next_sibling") # BS3
|
||||
previousSibling = _alias("previous_sibling") # BS3
|
||||
|
||||
def replace_with(self, replace_with):
|
||||
if not self.parent:
|
||||
raise ValueError(
|
||||
"Cannot replace one element with another when the"
|
||||
"element to be replaced is not part of a tree.")
|
||||
if replace_with is self:
|
||||
return
|
||||
if replace_with is self.parent:
|
||||
@@ -216,6 +233,10 @@ class PageElement(object):
|
||||
|
||||
def unwrap(self):
|
||||
my_parent = self.parent
|
||||
if not self.parent:
|
||||
raise ValueError(
|
||||
"Cannot replace an element with its contents when that"
|
||||
"element is not part of a tree.")
|
||||
my_index = self.parent.index(self)
|
||||
self.extract()
|
||||
for child in reversed(self.contents[:]):
|
||||
@@ -240,17 +261,20 @@ class PageElement(object):
|
||||
last_child = self._last_descendant()
|
||||
next_element = last_child.next_element
|
||||
|
||||
if self.previous_element is not None:
|
||||
if (self.previous_element is not None and
|
||||
self.previous_element != next_element):
|
||||
self.previous_element.next_element = next_element
|
||||
if next_element is not None:
|
||||
if next_element is not None and next_element != self.previous_element:
|
||||
next_element.previous_element = self.previous_element
|
||||
self.previous_element = None
|
||||
last_child.next_element = None
|
||||
|
||||
self.parent = None
|
||||
if self.previous_sibling is not None:
|
||||
if (self.previous_sibling is not None
|
||||
and self.previous_sibling != self.next_sibling):
|
||||
self.previous_sibling.next_sibling = self.next_sibling
|
||||
if self.next_sibling is not None:
|
||||
if (self.next_sibling is not None
|
||||
and self.next_sibling != self.previous_sibling):
|
||||
self.next_sibling.previous_sibling = self.previous_sibling
|
||||
self.previous_sibling = self.next_sibling = None
|
||||
return self
|
||||
@@ -478,6 +502,10 @@ class PageElement(object):
|
||||
def _find_all(self, name, attrs, text, limit, generator, **kwargs):
|
||||
"Iterates over a generator looking for things that match."
|
||||
|
||||
if text is None and 'string' in kwargs:
|
||||
text = kwargs['string']
|
||||
del kwargs['string']
|
||||
|
||||
if isinstance(name, SoupStrainer):
|
||||
strainer = name
|
||||
else:
|
||||
@@ -548,17 +576,17 @@ class PageElement(object):
|
||||
|
||||
# Methods for supporting CSS selectors.
|
||||
|
||||
tag_name_re = re.compile('^[a-z0-9]+$')
|
||||
tag_name_re = re.compile('^[a-zA-Z0-9][-.a-zA-Z0-9:_]*$')
|
||||
|
||||
# /^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
|
||||
# \---/ \---/\-------------/ \-------/
|
||||
# | | | |
|
||||
# | | | The value
|
||||
# | | ~,|,^,$,* or =
|
||||
# | Attribute
|
||||
# /^([a-zA-Z0-9][-.a-zA-Z0-9:_]*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
|
||||
# \---------------------------/ \---/\-------------/ \-------/
|
||||
# | | | |
|
||||
# | | | The value
|
||||
# | | ~,|,^,$,* or =
|
||||
# | Attribute
|
||||
# Tag
|
||||
attribselect_re = re.compile(
|
||||
r'^(?P<tag>\w+)?\[(?P<attribute>\w+)(?P<operator>[=~\|\^\$\*]?)' +
|
||||
r'^(?P<tag>[a-zA-Z0-9][-.a-zA-Z0-9:_]*)?\[(?P<attribute>[\w-]+)(?P<operator>[=~\|\^\$\*]?)' +
|
||||
r'=?"?(?P<value>[^\]"]*)"?\]$'
|
||||
)
|
||||
|
||||
@@ -654,11 +682,17 @@ class NavigableString(unicode, PageElement):
|
||||
how to handle non-ASCII characters.
|
||||
"""
|
||||
if isinstance(value, unicode):
|
||||
return unicode.__new__(cls, value)
|
||||
return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
|
||||
u = unicode.__new__(cls, value)
|
||||
else:
|
||||
u = unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING)
|
||||
u.setup()
|
||||
return u
|
||||
|
||||
def __copy__(self):
|
||||
return self
|
||||
"""A copy of a NavigableString has the same contents and class
|
||||
as the original, but it is not connected to the parse tree.
|
||||
"""
|
||||
return type(self)(self)
|
||||
|
||||
def __getnewargs__(self):
|
||||
return (unicode(self),)
|
||||
@@ -707,7 +741,7 @@ class CData(PreformattedString):
|
||||
class ProcessingInstruction(PreformattedString):
|
||||
|
||||
PREFIX = u'<?'
|
||||
SUFFIX = u'?>'
|
||||
SUFFIX = u'>'
|
||||
|
||||
class Comment(PreformattedString):
|
||||
|
||||
@@ -759,9 +793,12 @@ class Tag(PageElement):
|
||||
self.prefix = prefix
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
elif attrs and builder.cdata_list_attributes:
|
||||
attrs = builder._replace_cdata_list_attribute_values(
|
||||
self.name, attrs)
|
||||
elif attrs:
|
||||
if builder is not None and builder.cdata_list_attributes:
|
||||
attrs = builder._replace_cdata_list_attribute_values(
|
||||
self.name, attrs)
|
||||
else:
|
||||
attrs = dict(attrs)
|
||||
else:
|
||||
attrs = dict(attrs)
|
||||
self.attrs = attrs
|
||||
@@ -778,6 +815,18 @@ class Tag(PageElement):
|
||||
|
||||
parserClass = _alias("parser_class") # BS3
|
||||
|
||||
def __copy__(self):
|
||||
"""A copy of a Tag is a new Tag, unconnected to the parse tree.
|
||||
Its contents are a copy of the old Tag's contents.
|
||||
"""
|
||||
clone = type(self)(None, self.builder, self.name, self.namespace,
|
||||
self.nsprefix, self.attrs)
|
||||
for attr in ('can_be_empty_element', 'hidden'):
|
||||
setattr(clone, attr, getattr(self, attr))
|
||||
for child in self.contents:
|
||||
clone.append(child.__copy__())
|
||||
return clone
|
||||
|
||||
@property
|
||||
def is_empty_element(self):
|
||||
"""Is this tag an empty-element tag? (aka a self-closing tag)
|
||||
@@ -971,15 +1020,25 @@ class Tag(PageElement):
|
||||
as defined in __eq__."""
|
||||
return not self == other
|
||||
|
||||
def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING):
|
||||
def __repr__(self, encoding="unicode-escape"):
|
||||
"""Renders this tag as a string."""
|
||||
return self.encode(encoding)
|
||||
if PY3K:
|
||||
# "The return value must be a string object", i.e. Unicode
|
||||
return self.decode()
|
||||
else:
|
||||
# "The return value must be a string object", i.e. a bytestring.
|
||||
# By convention, the return value of __repr__ should also be
|
||||
# an ASCII string.
|
||||
return self.encode(encoding)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.decode()
|
||||
|
||||
def __str__(self):
|
||||
return self.encode()
|
||||
if PY3K:
|
||||
return self.decode()
|
||||
else:
|
||||
return self.encode()
|
||||
|
||||
if PY3K:
|
||||
__str__ = __repr__ = __unicode__
|
||||
@@ -1103,12 +1162,18 @@ class Tag(PageElement):
|
||||
formatter="minimal"):
|
||||
"""Renders the contents of this tag as a Unicode string.
|
||||
|
||||
:param indent_level: Each line of the rendering will be
|
||||
indented this many spaces.
|
||||
|
||||
:param eventual_encoding: The tag is destined to be
|
||||
encoded into this encoding. This method is _not_
|
||||
responsible for performing that encoding. This information
|
||||
is passed in so that it can be substituted in if the
|
||||
document contains a <META> tag that mentions the document's
|
||||
encoding.
|
||||
|
||||
:param formatter: The output formatter responsible for converting
|
||||
entities to Unicode characters.
|
||||
"""
|
||||
# First off, turn a string formatter into a function. This
|
||||
# will stop the lookup from happening over and over again.
|
||||
@@ -1137,7 +1202,17 @@ class Tag(PageElement):
|
||||
def encode_contents(
|
||||
self, indent_level=None, encoding=DEFAULT_OUTPUT_ENCODING,
|
||||
formatter="minimal"):
|
||||
"""Renders the contents of this tag as a bytestring."""
|
||||
"""Renders the contents of this tag as a bytestring.
|
||||
|
||||
:param indent_level: Each line of the rendering will be
|
||||
indented this many spaces.
|
||||
|
||||
:param eventual_encoding: The bytestring will be in this encoding.
|
||||
|
||||
:param formatter: The output formatter responsible for converting
|
||||
entities to Unicode characters.
|
||||
"""
|
||||
|
||||
contents = self.decode_contents(indent_level, encoding, formatter)
|
||||
return contents.encode(encoding)
|
||||
|
||||
@@ -1201,63 +1276,89 @@ class Tag(PageElement):
|
||||
|
||||
_selector_combinators = ['>', '+', '~']
|
||||
_select_debug = False
|
||||
def select(self, selector, _candidate_generator=None):
|
||||
def select_one(self, selector):
|
||||
"""Perform a CSS selection operation on the current element."""
|
||||
tokens = selector.split()
|
||||
value = self.select(selector, limit=1)
|
||||
if value:
|
||||
return value[0]
|
||||
return None
|
||||
|
||||
def select(self, selector, _candidate_generator=None, limit=None):
|
||||
"""Perform a CSS selection operation on the current element."""
|
||||
|
||||
# Remove whitespace directly after the grouping operator ','
|
||||
# then split into tokens.
|
||||
tokens = re.sub(',[\s]*',',', selector).split()
|
||||
current_context = [self]
|
||||
|
||||
if tokens[-1] in self._selector_combinators:
|
||||
raise ValueError(
|
||||
'Final combinator "%s" is missing an argument.' % tokens[-1])
|
||||
|
||||
if self._select_debug:
|
||||
print 'Running CSS selector "%s"' % selector
|
||||
for index, token in enumerate(tokens):
|
||||
if self._select_debug:
|
||||
print ' Considering token "%s"' % token
|
||||
recursive_candidate_generator = None
|
||||
tag_name = None
|
||||
|
||||
for index, token_group in enumerate(tokens):
|
||||
new_context = []
|
||||
new_context_ids = set([])
|
||||
|
||||
# Grouping selectors, ie: p,a
|
||||
grouped_tokens = token_group.split(',')
|
||||
if '' in grouped_tokens:
|
||||
raise ValueError('Invalid group selection syntax: %s' % token_group)
|
||||
|
||||
if tokens[index-1] in self._selector_combinators:
|
||||
# This token was consumed by the previous combinator. Skip it.
|
||||
if self._select_debug:
|
||||
print ' Token was consumed by the previous combinator.'
|
||||
continue
|
||||
# Each operation corresponds to a checker function, a rule
|
||||
# for determining whether a candidate matches the
|
||||
# selector. Candidates are generated by the active
|
||||
# iterator.
|
||||
checker = None
|
||||
|
||||
m = self.attribselect_re.match(token)
|
||||
if m is not None:
|
||||
# Attribute selector
|
||||
tag_name, attribute, operator, value = m.groups()
|
||||
checker = self._attribute_checker(operator, attribute, value)
|
||||
for token in grouped_tokens:
|
||||
if self._select_debug:
|
||||
print ' Considering token "%s"' % token
|
||||
recursive_candidate_generator = None
|
||||
tag_name = None
|
||||
|
||||
elif '#' in token:
|
||||
# ID selector
|
||||
tag_name, tag_id = token.split('#', 1)
|
||||
def id_matches(tag):
|
||||
return tag.get('id', None) == tag_id
|
||||
checker = id_matches
|
||||
# Each operation corresponds to a checker function, a rule
|
||||
# for determining whether a candidate matches the
|
||||
# selector. Candidates are generated by the active
|
||||
# iterator.
|
||||
checker = None
|
||||
|
||||
elif '.' in token:
|
||||
# Class selector
|
||||
tag_name, klass = token.split('.', 1)
|
||||
classes = set(klass.split('.'))
|
||||
def classes_match(candidate):
|
||||
return classes.issubset(candidate.get('class', []))
|
||||
checker = classes_match
|
||||
m = self.attribselect_re.match(token)
|
||||
if m is not None:
|
||||
# Attribute selector
|
||||
tag_name, attribute, operator, value = m.groups()
|
||||
checker = self._attribute_checker(operator, attribute, value)
|
||||
|
||||
elif ':' in token:
|
||||
# Pseudo-class
|
||||
tag_name, pseudo = token.split(':', 1)
|
||||
if tag_name == '':
|
||||
raise ValueError(
|
||||
"A pseudo-class must be prefixed with a tag name.")
|
||||
pseudo_attributes = re.match('([a-zA-Z\d-]+)\(([a-zA-Z\d]+)\)', pseudo)
|
||||
found = []
|
||||
if pseudo_attributes is not None:
|
||||
pseudo_type, pseudo_value = pseudo_attributes.groups()
|
||||
elif '#' in token:
|
||||
# ID selector
|
||||
tag_name, tag_id = token.split('#', 1)
|
||||
def id_matches(tag):
|
||||
return tag.get('id', None) == tag_id
|
||||
checker = id_matches
|
||||
|
||||
elif '.' in token:
|
||||
# Class selector
|
||||
tag_name, klass = token.split('.', 1)
|
||||
classes = set(klass.split('.'))
|
||||
def classes_match(candidate):
|
||||
return classes.issubset(candidate.get('class', []))
|
||||
checker = classes_match
|
||||
|
||||
elif ':' in token:
|
||||
# Pseudo-class
|
||||
tag_name, pseudo = token.split(':', 1)
|
||||
if tag_name == '':
|
||||
raise ValueError(
|
||||
"A pseudo-class must be prefixed with a tag name.")
|
||||
pseudo_attributes = re.match('([a-zA-Z\d-]+)\(([a-zA-Z\d]+)\)', pseudo)
|
||||
found = []
|
||||
if pseudo_attributes is None:
|
||||
pseudo_type = pseudo
|
||||
pseudo_value = None
|
||||
else:
|
||||
pseudo_type, pseudo_value = pseudo_attributes.groups()
|
||||
if pseudo_type == 'nth-of-type':
|
||||
try:
|
||||
pseudo_value = int(pseudo_value)
|
||||
@@ -1286,109 +1387,110 @@ class Tag(PageElement):
|
||||
raise NotImplementedError(
|
||||
'Only the following pseudo-classes are implemented: nth-of-type.')
|
||||
|
||||
elif token == '*':
|
||||
# Star selector -- matches everything
|
||||
pass
|
||||
elif token == '>':
|
||||
# Run the next token as a CSS selector against the
|
||||
# direct children of each tag in the current context.
|
||||
recursive_candidate_generator = lambda tag: tag.children
|
||||
elif token == '~':
|
||||
# Run the next token as a CSS selector against the
|
||||
# siblings of each tag in the current context.
|
||||
recursive_candidate_generator = lambda tag: tag.next_siblings
|
||||
elif token == '+':
|
||||
# For each tag in the current context, run the next
|
||||
# token as a CSS selector against the tag's next
|
||||
# sibling that's a tag.
|
||||
def next_tag_sibling(tag):
|
||||
yield tag.find_next_sibling(True)
|
||||
recursive_candidate_generator = next_tag_sibling
|
||||
elif token == '*':
|
||||
# Star selector -- matches everything
|
||||
pass
|
||||
elif token == '>':
|
||||
# Run the next token as a CSS selector against the
|
||||
# direct children of each tag in the current context.
|
||||
recursive_candidate_generator = lambda tag: tag.children
|
||||
elif token == '~':
|
||||
# Run the next token as a CSS selector against the
|
||||
# siblings of each tag in the current context.
|
||||
recursive_candidate_generator = lambda tag: tag.next_siblings
|
||||
elif token == '+':
|
||||
# For each tag in the current context, run the next
|
||||
# token as a CSS selector against the tag's next
|
||||
# sibling that's a tag.
|
||||
def next_tag_sibling(tag):
|
||||
yield tag.find_next_sibling(True)
|
||||
recursive_candidate_generator = next_tag_sibling
|
||||
|
||||
elif self.tag_name_re.match(token):
|
||||
# Just a tag name.
|
||||
tag_name = token
|
||||
else:
|
||||
raise ValueError(
|
||||
'Unsupported or invalid CSS selector: "%s"' % token)
|
||||
|
||||
if recursive_candidate_generator:
|
||||
# This happens when the selector looks like "> foo".
|
||||
#
|
||||
# The generator calls select() recursively on every
|
||||
# member of the current context, passing in a different
|
||||
# candidate generator and a different selector.
|
||||
#
|
||||
# In the case of "> foo", the candidate generator is
|
||||
# one that yields a tag's direct children (">"), and
|
||||
# the selector is "foo".
|
||||
next_token = tokens[index+1]
|
||||
def recursive_select(tag):
|
||||
if self._select_debug:
|
||||
print ' Calling select("%s") recursively on %s %s' % (next_token, tag.name, tag.attrs)
|
||||
print '-' * 40
|
||||
for i in tag.select(next_token, recursive_candidate_generator):
|
||||
if self._select_debug:
|
||||
print '(Recursive select picked up candidate %s %s)' % (i.name, i.attrs)
|
||||
yield i
|
||||
if self._select_debug:
|
||||
print '-' * 40
|
||||
_use_candidate_generator = recursive_select
|
||||
elif _candidate_generator is None:
|
||||
# By default, a tag's candidates are all of its
|
||||
# children. If tag_name is defined, only yield tags
|
||||
# with that name.
|
||||
if self._select_debug:
|
||||
if tag_name:
|
||||
check = "[any]"
|
||||
else:
|
||||
check = tag_name
|
||||
print ' Default candidate generator, tag name="%s"' % check
|
||||
if self._select_debug:
|
||||
# This is redundant with later code, but it stops
|
||||
# a bunch of bogus tags from cluttering up the
|
||||
# debug log.
|
||||
def default_candidate_generator(tag):
|
||||
for child in tag.descendants:
|
||||
if not isinstance(child, Tag):
|
||||
continue
|
||||
if tag_name and not child.name == tag_name:
|
||||
continue
|
||||
yield child
|
||||
_use_candidate_generator = default_candidate_generator
|
||||
elif self.tag_name_re.match(token):
|
||||
# Just a tag name.
|
||||
tag_name = token
|
||||
else:
|
||||
_use_candidate_generator = lambda tag: tag.descendants
|
||||
else:
|
||||
_use_candidate_generator = _candidate_generator
|
||||
|
||||
new_context = []
|
||||
new_context_ids = set([])
|
||||
for tag in current_context:
|
||||
if self._select_debug:
|
||||
print " Running candidate generator on %s %s" % (
|
||||
tag.name, repr(tag.attrs))
|
||||
for candidate in _use_candidate_generator(tag):
|
||||
if not isinstance(candidate, Tag):
|
||||
continue
|
||||
if tag_name and candidate.name != tag_name:
|
||||
continue
|
||||
if checker is not None:
|
||||
try:
|
||||
result = checker(candidate)
|
||||
except StopIteration:
|
||||
# The checker has decided we should no longer
|
||||
# run the generator.
|
||||
break
|
||||
if checker is None or result:
|
||||
raise ValueError(
|
||||
'Unsupported or invalid CSS selector: "%s"' % token)
|
||||
if recursive_candidate_generator:
|
||||
# This happens when the selector looks like "> foo".
|
||||
#
|
||||
# The generator calls select() recursively on every
|
||||
# member of the current context, passing in a different
|
||||
# candidate generator and a different selector.
|
||||
#
|
||||
# In the case of "> foo", the candidate generator is
|
||||
# one that yields a tag's direct children (">"), and
|
||||
# the selector is "foo".
|
||||
next_token = tokens[index+1]
|
||||
def recursive_select(tag):
|
||||
if self._select_debug:
|
||||
print " SUCCESS %s %s" % (candidate.name, repr(candidate.attrs))
|
||||
if id(candidate) not in new_context_ids:
|
||||
# If a tag matches a selector more than once,
|
||||
# don't include it in the context more than once.
|
||||
new_context.append(candidate)
|
||||
new_context_ids.add(id(candidate))
|
||||
elif self._select_debug:
|
||||
print " FAILURE %s %s" % (candidate.name, repr(candidate.attrs))
|
||||
print ' Calling select("%s") recursively on %s %s' % (next_token, tag.name, tag.attrs)
|
||||
print '-' * 40
|
||||
for i in tag.select(next_token, recursive_candidate_generator):
|
||||
if self._select_debug:
|
||||
print '(Recursive select picked up candidate %s %s)' % (i.name, i.attrs)
|
||||
yield i
|
||||
if self._select_debug:
|
||||
print '-' * 40
|
||||
_use_candidate_generator = recursive_select
|
||||
elif _candidate_generator is None:
|
||||
# By default, a tag's candidates are all of its
|
||||
# children. If tag_name is defined, only yield tags
|
||||
# with that name.
|
||||
if self._select_debug:
|
||||
if tag_name:
|
||||
check = "[any]"
|
||||
else:
|
||||
check = tag_name
|
||||
print ' Default candidate generator, tag name="%s"' % check
|
||||
if self._select_debug:
|
||||
# This is redundant with later code, but it stops
|
||||
# a bunch of bogus tags from cluttering up the
|
||||
# debug log.
|
||||
def default_candidate_generator(tag):
|
||||
for child in tag.descendants:
|
||||
if not isinstance(child, Tag):
|
||||
continue
|
||||
if tag_name and not child.name == tag_name:
|
||||
continue
|
||||
yield child
|
||||
_use_candidate_generator = default_candidate_generator
|
||||
else:
|
||||
_use_candidate_generator = lambda tag: tag.descendants
|
||||
else:
|
||||
_use_candidate_generator = _candidate_generator
|
||||
|
||||
count = 0
|
||||
for tag in current_context:
|
||||
if self._select_debug:
|
||||
print " Running candidate generator on %s %s" % (
|
||||
tag.name, repr(tag.attrs))
|
||||
for candidate in _use_candidate_generator(tag):
|
||||
if not isinstance(candidate, Tag):
|
||||
continue
|
||||
if tag_name and candidate.name != tag_name:
|
||||
continue
|
||||
if checker is not None:
|
||||
try:
|
||||
result = checker(candidate)
|
||||
except StopIteration:
|
||||
# The checker has decided we should no longer
|
||||
# run the generator.
|
||||
break
|
||||
if checker is None or result:
|
||||
if self._select_debug:
|
||||
print " SUCCESS %s %s" % (candidate.name, repr(candidate.attrs))
|
||||
if id(candidate) not in new_context_ids:
|
||||
# If a tag matches a selector more than once,
|
||||
# don't include it in the context more than once.
|
||||
new_context.append(candidate)
|
||||
new_context_ids.add(id(candidate))
|
||||
if limit and len(new_context) >= limit:
|
||||
break
|
||||
elif self._select_debug:
|
||||
print " FAILURE %s %s" % (candidate.name, repr(candidate.attrs))
|
||||
|
||||
|
||||
current_context = new_context
|
||||
|
||||
|
||||
+89
-1
@@ -1,5 +1,6 @@
|
||||
"""Helper classes for tests."""
|
||||
|
||||
import pickle
|
||||
import copy
|
||||
import functools
|
||||
import unittest
|
||||
@@ -43,6 +44,16 @@ class SoupTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(obj.decode(), self.document_for(compare_parsed_to))
|
||||
|
||||
def assertConnectedness(self, element):
|
||||
"""Ensure that next_element and previous_element are properly
|
||||
set for all descendants of the given element.
|
||||
"""
|
||||
earlier = None
|
||||
for e in element.descendants:
|
||||
if earlier:
|
||||
self.assertEqual(e, earlier.next_element)
|
||||
self.assertEqual(earlier, e.previous_element)
|
||||
earlier = e
|
||||
|
||||
class HTMLTreeBuilderSmokeTest(object):
|
||||
|
||||
@@ -54,6 +65,15 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||
markup in these tests, there's not much room for interpretation.
|
||||
"""
|
||||
|
||||
def test_pickle_and_unpickle_identity(self):
|
||||
# Pickling a tree, then unpickling it, yields a tree identical
|
||||
# to the original.
|
||||
tree = self.soup("<a><b>foo</a>")
|
||||
dumped = pickle.dumps(tree, 2)
|
||||
loaded = pickle.loads(dumped)
|
||||
self.assertEqual(loaded.__class__, BeautifulSoup)
|
||||
self.assertEqual(loaded.decode(), tree.decode())
|
||||
|
||||
def assertDoctypeHandled(self, doctype_fragment):
|
||||
"""Assert that a given doctype string is handled correctly."""
|
||||
doctype_str, soup = self._document_with_doctype(doctype_fragment)
|
||||
@@ -114,6 +134,11 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||
soup.encode("utf-8").replace(b"\n", b""),
|
||||
markup.replace(b"\n", b""))
|
||||
|
||||
def test_processing_instruction(self):
|
||||
markup = b"""<?PITarget PIContent?>"""
|
||||
soup = self.soup(markup)
|
||||
self.assertEqual(markup, soup.encode("utf8"))
|
||||
|
||||
def test_deepcopy(self):
|
||||
"""Make sure you can copy the tree builder.
|
||||
|
||||
@@ -155,6 +180,23 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||
def test_nested_formatting_elements(self):
|
||||
self.assertSoupEquals("<em><em></em></em>")
|
||||
|
||||
def test_double_head(self):
|
||||
html = '''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ordinary HEAD element test</title>
|
||||
</head>
|
||||
<script type="text/javascript">
|
||||
alert("Help!");
|
||||
</script>
|
||||
<body>
|
||||
Hello, world!
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
soup = self.soup(html)
|
||||
self.assertEqual("text/javascript", soup.find('script')['type'])
|
||||
|
||||
def test_comment(self):
|
||||
# Comments are represented as Comment objects.
|
||||
markup = "<p>foo<!--foobar-->baz</p>"
|
||||
@@ -221,6 +263,14 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||
soup = self.soup(markup)
|
||||
self.assertEqual(["css"], soup.div.div['class'])
|
||||
|
||||
def test_multivalued_attribute_on_html(self):
|
||||
# html5lib uses a different API to set the attributes ot the
|
||||
# <html> tag. This has caused problems with multivalued
|
||||
# attributes.
|
||||
markup = '<html class="a b"></html>'
|
||||
soup = self.soup(markup)
|
||||
self.assertEqual(["a", "b"], soup.html['class'])
|
||||
|
||||
def test_angle_brackets_in_attribute_values_are_escaped(self):
|
||||
self.assertSoupEquals('<a b="<a>"></a>', '<a b="<a>"></a>')
|
||||
|
||||
@@ -253,6 +303,35 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||
soup = self.soup("<html><h2>\nfoo</h2><p></p></html>")
|
||||
self.assertEqual("p", soup.h2.string.next_element.name)
|
||||
self.assertEqual("p", soup.p.name)
|
||||
self.assertConnectedness(soup)
|
||||
|
||||
def test_head_tag_between_head_and_body(self):
|
||||
"Prevent recurrence of a bug in the html5lib treebuilder."
|
||||
content = """<html><head></head>
|
||||
<link></link>
|
||||
<body>foo</body>
|
||||
</html>
|
||||
"""
|
||||
soup = self.soup(content)
|
||||
self.assertNotEqual(None, soup.html.body)
|
||||
self.assertConnectedness(soup)
|
||||
|
||||
def test_multiple_copies_of_a_tag(self):
|
||||
"Prevent recurrence of a bug in the html5lib treebuilder."
|
||||
content = """<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<article id="a" >
|
||||
<div><a href="1"></div>
|
||||
<footer>
|
||||
<a href="2"></a>
|
||||
</footer>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
soup = self.soup(content)
|
||||
self.assertConnectedness(soup.article)
|
||||
|
||||
def test_basic_namespaces(self):
|
||||
"""Parsers don't need to *understand* namespaces, but at the
|
||||
@@ -463,6 +542,15 @@ class HTMLTreeBuilderSmokeTest(object):
|
||||
|
||||
class XMLTreeBuilderSmokeTest(object):
|
||||
|
||||
def test_pickle_and_unpickle_identity(self):
|
||||
# Pickling a tree, then unpickling it, yields a tree identical
|
||||
# to the original.
|
||||
tree = self.soup("<a><b>foo</a>")
|
||||
dumped = pickle.dumps(tree, 2)
|
||||
loaded = pickle.loads(dumped)
|
||||
self.assertEqual(loaded.__class__, BeautifulSoup)
|
||||
self.assertEqual(loaded.decode(), tree.decode())
|
||||
|
||||
def test_docstring_generated(self):
|
||||
soup = self.soup("<root/>")
|
||||
self.assertEqual(
|
||||
@@ -485,7 +573,7 @@ class XMLTreeBuilderSmokeTest(object):
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
"""
|
||||
soup = BeautifulSoup(doc, "xml")
|
||||
soup = BeautifulSoup(doc, "lxml-xml")
|
||||
# lxml would have stripped this while parsing, but we can add
|
||||
# it later.
|
||||
soup.script.string = 'console.log("< < hey > > ");'
|
||||
|
||||
@@ -99,12 +99,14 @@ DEFAULT_BUFFER_SIZE = -1
|
||||
|
||||
|
||||
class FauxSocket(object):
|
||||
|
||||
"""Faux socket with the minimal interface required by pypy"""
|
||||
|
||||
def _reuse(self):
|
||||
pass
|
||||
|
||||
def _drop(self):
|
||||
pass
|
||||
|
||||
_fileobject_uses_str_type = isinstance(
|
||||
socket._fileobject(FauxSocket())._rbuf, basestring)
|
||||
del FauxSocket # this class is not longer required for anything.
|
||||
|
||||
@@ -20,4 +20,6 @@ from .serializer import serialize
|
||||
|
||||
__all__ = ["HTMLParser", "parse", "parseFragment", "getTreeBuilder",
|
||||
"getTreeWalker", "serialize"]
|
||||
__version__ = "0.999"
|
||||
|
||||
# this has to be at the top level, see how setup.py parses this
|
||||
__version__ = "0.999999"
|
||||
|
||||
+193
-195
@@ -1,292 +1,290 @@
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import string
|
||||
import gettext
|
||||
_ = gettext.gettext
|
||||
|
||||
EOF = None
|
||||
|
||||
E = {
|
||||
"null-character":
|
||||
_("Null character in input stream, replaced with U+FFFD."),
|
||||
"Null character in input stream, replaced with U+FFFD.",
|
||||
"invalid-codepoint":
|
||||
_("Invalid codepoint in stream."),
|
||||
"Invalid codepoint in stream.",
|
||||
"incorrectly-placed-solidus":
|
||||
_("Solidus (/) incorrectly placed in tag."),
|
||||
"Solidus (/) incorrectly placed in tag.",
|
||||
"incorrect-cr-newline-entity":
|
||||
_("Incorrect CR newline entity, replaced with LF."),
|
||||
"Incorrect CR newline entity, replaced with LF.",
|
||||
"illegal-windows-1252-entity":
|
||||
_("Entity used with illegal number (windows-1252 reference)."),
|
||||
"Entity used with illegal number (windows-1252 reference).",
|
||||
"cant-convert-numeric-entity":
|
||||
_("Numeric entity couldn't be converted to character "
|
||||
"(codepoint U+%(charAsInt)08x)."),
|
||||
"Numeric entity couldn't be converted to character "
|
||||
"(codepoint U+%(charAsInt)08x).",
|
||||
"illegal-codepoint-for-numeric-entity":
|
||||
_("Numeric entity represents an illegal codepoint: "
|
||||
"U+%(charAsInt)08x."),
|
||||
"Numeric entity represents an illegal codepoint: "
|
||||
"U+%(charAsInt)08x.",
|
||||
"numeric-entity-without-semicolon":
|
||||
_("Numeric entity didn't end with ';'."),
|
||||
"Numeric entity didn't end with ';'.",
|
||||
"expected-numeric-entity-but-got-eof":
|
||||
_("Numeric entity expected. Got end of file instead."),
|
||||
"Numeric entity expected. Got end of file instead.",
|
||||
"expected-numeric-entity":
|
||||
_("Numeric entity expected but none found."),
|
||||
"Numeric entity expected but none found.",
|
||||
"named-entity-without-semicolon":
|
||||
_("Named entity didn't end with ';'."),
|
||||
"Named entity didn't end with ';'.",
|
||||
"expected-named-entity":
|
||||
_("Named entity expected. Got none."),
|
||||
"Named entity expected. Got none.",
|
||||
"attributes-in-end-tag":
|
||||
_("End tag contains unexpected attributes."),
|
||||
"End tag contains unexpected attributes.",
|
||||
'self-closing-flag-on-end-tag':
|
||||
_("End tag contains unexpected self-closing flag."),
|
||||
"End tag contains unexpected self-closing flag.",
|
||||
"expected-tag-name-but-got-right-bracket":
|
||||
_("Expected tag name. Got '>' instead."),
|
||||
"Expected tag name. Got '>' instead.",
|
||||
"expected-tag-name-but-got-question-mark":
|
||||
_("Expected tag name. Got '?' instead. (HTML doesn't "
|
||||
"support processing instructions.)"),
|
||||
"Expected tag name. Got '?' instead. (HTML doesn't "
|
||||
"support processing instructions.)",
|
||||
"expected-tag-name":
|
||||
_("Expected tag name. Got something else instead"),
|
||||
"Expected tag name. Got something else instead",
|
||||
"expected-closing-tag-but-got-right-bracket":
|
||||
_("Expected closing tag. Got '>' instead. Ignoring '</>'."),
|
||||
"Expected closing tag. Got '>' instead. Ignoring '</>'.",
|
||||
"expected-closing-tag-but-got-eof":
|
||||
_("Expected closing tag. Unexpected end of file."),
|
||||
"Expected closing tag. Unexpected end of file.",
|
||||
"expected-closing-tag-but-got-char":
|
||||
_("Expected closing tag. Unexpected character '%(data)s' found."),
|
||||
"Expected closing tag. Unexpected character '%(data)s' found.",
|
||||
"eof-in-tag-name":
|
||||
_("Unexpected end of file in the tag name."),
|
||||
"Unexpected end of file in the tag name.",
|
||||
"expected-attribute-name-but-got-eof":
|
||||
_("Unexpected end of file. Expected attribute name instead."),
|
||||
"Unexpected end of file. Expected attribute name instead.",
|
||||
"eof-in-attribute-name":
|
||||
_("Unexpected end of file in attribute name."),
|
||||
"Unexpected end of file in attribute name.",
|
||||
"invalid-character-in-attribute-name":
|
||||
_("Invalid character in attribute name"),
|
||||
"Invalid character in attribute name",
|
||||
"duplicate-attribute":
|
||||
_("Dropped duplicate attribute on tag."),
|
||||
"Dropped duplicate attribute on tag.",
|
||||
"expected-end-of-tag-name-but-got-eof":
|
||||
_("Unexpected end of file. Expected = or end of tag."),
|
||||
"Unexpected end of file. Expected = or end of tag.",
|
||||
"expected-attribute-value-but-got-eof":
|
||||
_("Unexpected end of file. Expected attribute value."),
|
||||
"Unexpected end of file. Expected attribute value.",
|
||||
"expected-attribute-value-but-got-right-bracket":
|
||||
_("Expected attribute value. Got '>' instead."),
|
||||
"Expected attribute value. Got '>' instead.",
|
||||
'equals-in-unquoted-attribute-value':
|
||||
_("Unexpected = in unquoted attribute"),
|
||||
"Unexpected = in unquoted attribute",
|
||||
'unexpected-character-in-unquoted-attribute-value':
|
||||
_("Unexpected character in unquoted attribute"),
|
||||
"Unexpected character in unquoted attribute",
|
||||
"invalid-character-after-attribute-name":
|
||||
_("Unexpected character after attribute name."),
|
||||
"Unexpected character after attribute name.",
|
||||
"unexpected-character-after-attribute-value":
|
||||
_("Unexpected character after attribute value."),
|
||||
"Unexpected character after attribute value.",
|
||||
"eof-in-attribute-value-double-quote":
|
||||
_("Unexpected end of file in attribute value (\")."),
|
||||
"Unexpected end of file in attribute value (\").",
|
||||
"eof-in-attribute-value-single-quote":
|
||||
_("Unexpected end of file in attribute value (')."),
|
||||
"Unexpected end of file in attribute value (').",
|
||||
"eof-in-attribute-value-no-quotes":
|
||||
_("Unexpected end of file in attribute value."),
|
||||
"Unexpected end of file in attribute value.",
|
||||
"unexpected-EOF-after-solidus-in-tag":
|
||||
_("Unexpected end of file in tag. Expected >"),
|
||||
"Unexpected end of file in tag. Expected >",
|
||||
"unexpected-character-after-solidus-in-tag":
|
||||
_("Unexpected character after / in tag. Expected >"),
|
||||
"Unexpected character after / in tag. Expected >",
|
||||
"expected-dashes-or-doctype":
|
||||
_("Expected '--' or 'DOCTYPE'. Not found."),
|
||||
"Expected '--' or 'DOCTYPE'. Not found.",
|
||||
"unexpected-bang-after-double-dash-in-comment":
|
||||
_("Unexpected ! after -- in comment"),
|
||||
"Unexpected ! after -- in comment",
|
||||
"unexpected-space-after-double-dash-in-comment":
|
||||
_("Unexpected space after -- in comment"),
|
||||
"Unexpected space after -- in comment",
|
||||
"incorrect-comment":
|
||||
_("Incorrect comment."),
|
||||
"Incorrect comment.",
|
||||
"eof-in-comment":
|
||||
_("Unexpected end of file in comment."),
|
||||
"Unexpected end of file in comment.",
|
||||
"eof-in-comment-end-dash":
|
||||
_("Unexpected end of file in comment (-)"),
|
||||
"Unexpected end of file in comment (-)",
|
||||
"unexpected-dash-after-double-dash-in-comment":
|
||||
_("Unexpected '-' after '--' found in comment."),
|
||||
"Unexpected '-' after '--' found in comment.",
|
||||
"eof-in-comment-double-dash":
|
||||
_("Unexpected end of file in comment (--)."),
|
||||
"Unexpected end of file in comment (--).",
|
||||
"eof-in-comment-end-space-state":
|
||||
_("Unexpected end of file in comment."),
|
||||
"Unexpected end of file in comment.",
|
||||
"eof-in-comment-end-bang-state":
|
||||
_("Unexpected end of file in comment."),
|
||||
"Unexpected end of file in comment.",
|
||||
"unexpected-char-in-comment":
|
||||
_("Unexpected character in comment found."),
|
||||
"Unexpected character in comment found.",
|
||||
"need-space-after-doctype":
|
||||
_("No space after literal string 'DOCTYPE'."),
|
||||
"No space after literal string 'DOCTYPE'.",
|
||||
"expected-doctype-name-but-got-right-bracket":
|
||||
_("Unexpected > character. Expected DOCTYPE name."),
|
||||
"Unexpected > character. Expected DOCTYPE name.",
|
||||
"expected-doctype-name-but-got-eof":
|
||||
_("Unexpected end of file. Expected DOCTYPE name."),
|
||||
"Unexpected end of file. Expected DOCTYPE name.",
|
||||
"eof-in-doctype-name":
|
||||
_("Unexpected end of file in DOCTYPE name."),
|
||||
"Unexpected end of file in DOCTYPE name.",
|
||||
"eof-in-doctype":
|
||||
_("Unexpected end of file in DOCTYPE."),
|
||||
"Unexpected end of file in DOCTYPE.",
|
||||
"expected-space-or-right-bracket-in-doctype":
|
||||
_("Expected space or '>'. Got '%(data)s'"),
|
||||
"Expected space or '>'. Got '%(data)s'",
|
||||
"unexpected-end-of-doctype":
|
||||
_("Unexpected end of DOCTYPE."),
|
||||
"Unexpected end of DOCTYPE.",
|
||||
"unexpected-char-in-doctype":
|
||||
_("Unexpected character in DOCTYPE."),
|
||||
"Unexpected character in DOCTYPE.",
|
||||
"eof-in-innerhtml":
|
||||
_("XXX innerHTML EOF"),
|
||||
"XXX innerHTML EOF",
|
||||
"unexpected-doctype":
|
||||
_("Unexpected DOCTYPE. Ignored."),
|
||||
"Unexpected DOCTYPE. Ignored.",
|
||||
"non-html-root":
|
||||
_("html needs to be the first start tag."),
|
||||
"html needs to be the first start tag.",
|
||||
"expected-doctype-but-got-eof":
|
||||
_("Unexpected End of file. Expected DOCTYPE."),
|
||||
"Unexpected End of file. Expected DOCTYPE.",
|
||||
"unknown-doctype":
|
||||
_("Erroneous DOCTYPE."),
|
||||
"Erroneous DOCTYPE.",
|
||||
"expected-doctype-but-got-chars":
|
||||
_("Unexpected non-space characters. Expected DOCTYPE."),
|
||||
"Unexpected non-space characters. Expected DOCTYPE.",
|
||||
"expected-doctype-but-got-start-tag":
|
||||
_("Unexpected start tag (%(name)s). Expected DOCTYPE."),
|
||||
"Unexpected start tag (%(name)s). Expected DOCTYPE.",
|
||||
"expected-doctype-but-got-end-tag":
|
||||
_("Unexpected end tag (%(name)s). Expected DOCTYPE."),
|
||||
"Unexpected end tag (%(name)s). Expected DOCTYPE.",
|
||||
"end-tag-after-implied-root":
|
||||
_("Unexpected end tag (%(name)s) after the (implied) root element."),
|
||||
"Unexpected end tag (%(name)s) after the (implied) root element.",
|
||||
"expected-named-closing-tag-but-got-eof":
|
||||
_("Unexpected end of file. Expected end tag (%(name)s)."),
|
||||
"Unexpected end of file. Expected end tag (%(name)s).",
|
||||
"two-heads-are-not-better-than-one":
|
||||
_("Unexpected start tag head in existing head. Ignored."),
|
||||
"Unexpected start tag head in existing head. Ignored.",
|
||||
"unexpected-end-tag":
|
||||
_("Unexpected end tag (%(name)s). Ignored."),
|
||||
"Unexpected end tag (%(name)s). Ignored.",
|
||||
"unexpected-start-tag-out-of-my-head":
|
||||
_("Unexpected start tag (%(name)s) that can be in head. Moved."),
|
||||
"Unexpected start tag (%(name)s) that can be in head. Moved.",
|
||||
"unexpected-start-tag":
|
||||
_("Unexpected start tag (%(name)s)."),
|
||||
"Unexpected start tag (%(name)s).",
|
||||
"missing-end-tag":
|
||||
_("Missing end tag (%(name)s)."),
|
||||
"Missing end tag (%(name)s).",
|
||||
"missing-end-tags":
|
||||
_("Missing end tags (%(name)s)."),
|
||||
"Missing end tags (%(name)s).",
|
||||
"unexpected-start-tag-implies-end-tag":
|
||||
_("Unexpected start tag (%(startName)s) "
|
||||
"implies end tag (%(endName)s)."),
|
||||
"Unexpected start tag (%(startName)s) "
|
||||
"implies end tag (%(endName)s).",
|
||||
"unexpected-start-tag-treated-as":
|
||||
_("Unexpected start tag (%(originalName)s). Treated as %(newName)s."),
|
||||
"Unexpected start tag (%(originalName)s). Treated as %(newName)s.",
|
||||
"deprecated-tag":
|
||||
_("Unexpected start tag %(name)s. Don't use it!"),
|
||||
"Unexpected start tag %(name)s. Don't use it!",
|
||||
"unexpected-start-tag-ignored":
|
||||
_("Unexpected start tag %(name)s. Ignored."),
|
||||
"Unexpected start tag %(name)s. Ignored.",
|
||||
"expected-one-end-tag-but-got-another":
|
||||
_("Unexpected end tag (%(gotName)s). "
|
||||
"Missing end tag (%(expectedName)s)."),
|
||||
"Unexpected end tag (%(gotName)s). "
|
||||
"Missing end tag (%(expectedName)s).",
|
||||
"end-tag-too-early":
|
||||
_("End tag (%(name)s) seen too early. Expected other end tag."),
|
||||
"End tag (%(name)s) seen too early. Expected other end tag.",
|
||||
"end-tag-too-early-named":
|
||||
_("Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s)."),
|
||||
"Unexpected end tag (%(gotName)s). Expected end tag (%(expectedName)s).",
|
||||
"end-tag-too-early-ignored":
|
||||
_("End tag (%(name)s) seen too early. Ignored."),
|
||||
"End tag (%(name)s) seen too early. Ignored.",
|
||||
"adoption-agency-1.1":
|
||||
_("End tag (%(name)s) violates step 1, "
|
||||
"paragraph 1 of the adoption agency algorithm."),
|
||||
"End tag (%(name)s) violates step 1, "
|
||||
"paragraph 1 of the adoption agency algorithm.",
|
||||
"adoption-agency-1.2":
|
||||
_("End tag (%(name)s) violates step 1, "
|
||||
"paragraph 2 of the adoption agency algorithm."),
|
||||
"End tag (%(name)s) violates step 1, "
|
||||
"paragraph 2 of the adoption agency algorithm.",
|
||||
"adoption-agency-1.3":
|
||||
_("End tag (%(name)s) violates step 1, "
|
||||
"paragraph 3 of the adoption agency algorithm."),
|
||||
"End tag (%(name)s) violates step 1, "
|
||||
"paragraph 3 of the adoption agency algorithm.",
|
||||
"adoption-agency-4.4":
|
||||
_("End tag (%(name)s) violates step 4, "
|
||||
"paragraph 4 of the adoption agency algorithm."),
|
||||
"End tag (%(name)s) violates step 4, "
|
||||
"paragraph 4 of the adoption agency algorithm.",
|
||||
"unexpected-end-tag-treated-as":
|
||||
_("Unexpected end tag (%(originalName)s). Treated as %(newName)s."),
|
||||
"Unexpected end tag (%(originalName)s). Treated as %(newName)s.",
|
||||
"no-end-tag":
|
||||
_("This element (%(name)s) has no end tag."),
|
||||
"This element (%(name)s) has no end tag.",
|
||||
"unexpected-implied-end-tag-in-table":
|
||||
_("Unexpected implied end tag (%(name)s) in the table phase."),
|
||||
"Unexpected implied end tag (%(name)s) in the table phase.",
|
||||
"unexpected-implied-end-tag-in-table-body":
|
||||
_("Unexpected implied end tag (%(name)s) in the table body phase."),
|
||||
"Unexpected implied end tag (%(name)s) in the table body phase.",
|
||||
"unexpected-char-implies-table-voodoo":
|
||||
_("Unexpected non-space characters in "
|
||||
"table context caused voodoo mode."),
|
||||
"Unexpected non-space characters in "
|
||||
"table context caused voodoo mode.",
|
||||
"unexpected-hidden-input-in-table":
|
||||
_("Unexpected input with type hidden in table context."),
|
||||
"Unexpected input with type hidden in table context.",
|
||||
"unexpected-form-in-table":
|
||||
_("Unexpected form in table context."),
|
||||
"Unexpected form in table context.",
|
||||
"unexpected-start-tag-implies-table-voodoo":
|
||||
_("Unexpected start tag (%(name)s) in "
|
||||
"table context caused voodoo mode."),
|
||||
"Unexpected start tag (%(name)s) in "
|
||||
"table context caused voodoo mode.",
|
||||
"unexpected-end-tag-implies-table-voodoo":
|
||||
_("Unexpected end tag (%(name)s) in "
|
||||
"table context caused voodoo mode."),
|
||||
"Unexpected end tag (%(name)s) in "
|
||||
"table context caused voodoo mode.",
|
||||
"unexpected-cell-in-table-body":
|
||||
_("Unexpected table cell start tag (%(name)s) "
|
||||
"in the table body phase."),
|
||||
"Unexpected table cell start tag (%(name)s) "
|
||||
"in the table body phase.",
|
||||
"unexpected-cell-end-tag":
|
||||
_("Got table cell end tag (%(name)s) "
|
||||
"while required end tags are missing."),
|
||||
"Got table cell end tag (%(name)s) "
|
||||
"while required end tags are missing.",
|
||||
"unexpected-end-tag-in-table-body":
|
||||
_("Unexpected end tag (%(name)s) in the table body phase. Ignored."),
|
||||
"Unexpected end tag (%(name)s) in the table body phase. Ignored.",
|
||||
"unexpected-implied-end-tag-in-table-row":
|
||||
_("Unexpected implied end tag (%(name)s) in the table row phase."),
|
||||
"Unexpected implied end tag (%(name)s) in the table row phase.",
|
||||
"unexpected-end-tag-in-table-row":
|
||||
_("Unexpected end tag (%(name)s) in the table row phase. Ignored."),
|
||||
"Unexpected end tag (%(name)s) in the table row phase. Ignored.",
|
||||
"unexpected-select-in-select":
|
||||
_("Unexpected select start tag in the select phase "
|
||||
"treated as select end tag."),
|
||||
"Unexpected select start tag in the select phase "
|
||||
"treated as select end tag.",
|
||||
"unexpected-input-in-select":
|
||||
_("Unexpected input start tag in the select phase."),
|
||||
"Unexpected input start tag in the select phase.",
|
||||
"unexpected-start-tag-in-select":
|
||||
_("Unexpected start tag token (%(name)s in the select phase. "
|
||||
"Ignored."),
|
||||
"Unexpected start tag token (%(name)s in the select phase. "
|
||||
"Ignored.",
|
||||
"unexpected-end-tag-in-select":
|
||||
_("Unexpected end tag (%(name)s) in the select phase. Ignored."),
|
||||
"Unexpected end tag (%(name)s) in the select phase. Ignored.",
|
||||
"unexpected-table-element-start-tag-in-select-in-table":
|
||||
_("Unexpected table element start tag (%(name)s) in the select in table phase."),
|
||||
"Unexpected table element start tag (%(name)s) in the select in table phase.",
|
||||
"unexpected-table-element-end-tag-in-select-in-table":
|
||||
_("Unexpected table element end tag (%(name)s) in the select in table phase."),
|
||||
"Unexpected table element end tag (%(name)s) in the select in table phase.",
|
||||
"unexpected-char-after-body":
|
||||
_("Unexpected non-space characters in the after body phase."),
|
||||
"Unexpected non-space characters in the after body phase.",
|
||||
"unexpected-start-tag-after-body":
|
||||
_("Unexpected start tag token (%(name)s)"
|
||||
" in the after body phase."),
|
||||
"Unexpected start tag token (%(name)s)"
|
||||
" in the after body phase.",
|
||||
"unexpected-end-tag-after-body":
|
||||
_("Unexpected end tag token (%(name)s)"
|
||||
" in the after body phase."),
|
||||
"Unexpected end tag token (%(name)s)"
|
||||
" in the after body phase.",
|
||||
"unexpected-char-in-frameset":
|
||||
_("Unexpected characters in the frameset phase. Characters ignored."),
|
||||
"Unexpected characters in the frameset phase. Characters ignored.",
|
||||
"unexpected-start-tag-in-frameset":
|
||||
_("Unexpected start tag token (%(name)s)"
|
||||
" in the frameset phase. Ignored."),
|
||||
"Unexpected start tag token (%(name)s)"
|
||||
" in the frameset phase. Ignored.",
|
||||
"unexpected-frameset-in-frameset-innerhtml":
|
||||
_("Unexpected end tag token (frameset) "
|
||||
"in the frameset phase (innerHTML)."),
|
||||
"Unexpected end tag token (frameset) "
|
||||
"in the frameset phase (innerHTML).",
|
||||
"unexpected-end-tag-in-frameset":
|
||||
_("Unexpected end tag token (%(name)s)"
|
||||
" in the frameset phase. Ignored."),
|
||||
"Unexpected end tag token (%(name)s)"
|
||||
" in the frameset phase. Ignored.",
|
||||
"unexpected-char-after-frameset":
|
||||
_("Unexpected non-space characters in the "
|
||||
"after frameset phase. Ignored."),
|
||||
"Unexpected non-space characters in the "
|
||||
"after frameset phase. Ignored.",
|
||||
"unexpected-start-tag-after-frameset":
|
||||
_("Unexpected start tag (%(name)s)"
|
||||
" in the after frameset phase. Ignored."),
|
||||
"Unexpected start tag (%(name)s)"
|
||||
" in the after frameset phase. Ignored.",
|
||||
"unexpected-end-tag-after-frameset":
|
||||
_("Unexpected end tag (%(name)s)"
|
||||
" in the after frameset phase. Ignored."),
|
||||
"Unexpected end tag (%(name)s)"
|
||||
" in the after frameset phase. Ignored.",
|
||||
"unexpected-end-tag-after-body-innerhtml":
|
||||
_("Unexpected end tag after body(innerHtml)"),
|
||||
"Unexpected end tag after body(innerHtml)",
|
||||
"expected-eof-but-got-char":
|
||||
_("Unexpected non-space characters. Expected end of file."),
|
||||
"Unexpected non-space characters. Expected end of file.",
|
||||
"expected-eof-but-got-start-tag":
|
||||
_("Unexpected start tag (%(name)s)"
|
||||
". Expected end of file."),
|
||||
"Unexpected start tag (%(name)s)"
|
||||
". Expected end of file.",
|
||||
"expected-eof-but-got-end-tag":
|
||||
_("Unexpected end tag (%(name)s)"
|
||||
". Expected end of file."),
|
||||
"Unexpected end tag (%(name)s)"
|
||||
". Expected end of file.",
|
||||
"eof-in-table":
|
||||
_("Unexpected end of file. Expected table content."),
|
||||
"Unexpected end of file. Expected table content.",
|
||||
"eof-in-select":
|
||||
_("Unexpected end of file. Expected select content."),
|
||||
"Unexpected end of file. Expected select content.",
|
||||
"eof-in-frameset":
|
||||
_("Unexpected end of file. Expected frameset content."),
|
||||
"Unexpected end of file. Expected frameset content.",
|
||||
"eof-in-script-in-script":
|
||||
_("Unexpected end of file. Expected script content."),
|
||||
"Unexpected end of file. Expected script content.",
|
||||
"eof-in-foreign-lands":
|
||||
_("Unexpected end of file. Expected foreign content"),
|
||||
"Unexpected end of file. Expected foreign content",
|
||||
"non-void-element-with-trailing-solidus":
|
||||
_("Trailing solidus not allowed on element %(name)s"),
|
||||
"Trailing solidus not allowed on element %(name)s",
|
||||
"unexpected-html-element-in-foreign-content":
|
||||
_("Element %(name)s not allowed in a non-html context"),
|
||||
"Element %(name)s not allowed in a non-html context",
|
||||
"unexpected-end-tag-before-html":
|
||||
_("Unexpected end tag (%(name)s) before html."),
|
||||
"Unexpected end tag (%(name)s) before html.",
|
||||
"XXX-undefined-error":
|
||||
_("Undefined error (this sucks and should be fixed)"),
|
||||
"Undefined error (this sucks and should be fixed)",
|
||||
}
|
||||
|
||||
namespaces = {
|
||||
@@ -298,7 +296,7 @@ namespaces = {
|
||||
"xmlns": "http://www.w3.org/2000/xmlns/"
|
||||
}
|
||||
|
||||
scopingElements = frozenset((
|
||||
scopingElements = frozenset([
|
||||
(namespaces["html"], "applet"),
|
||||
(namespaces["html"], "caption"),
|
||||
(namespaces["html"], "html"),
|
||||
@@ -316,9 +314,9 @@ scopingElements = frozenset((
|
||||
(namespaces["svg"], "foreignObject"),
|
||||
(namespaces["svg"], "desc"),
|
||||
(namespaces["svg"], "title"),
|
||||
))
|
||||
])
|
||||
|
||||
formattingElements = frozenset((
|
||||
formattingElements = frozenset([
|
||||
(namespaces["html"], "a"),
|
||||
(namespaces["html"], "b"),
|
||||
(namespaces["html"], "big"),
|
||||
@@ -333,9 +331,9 @@ formattingElements = frozenset((
|
||||
(namespaces["html"], "strong"),
|
||||
(namespaces["html"], "tt"),
|
||||
(namespaces["html"], "u")
|
||||
))
|
||||
])
|
||||
|
||||
specialElements = frozenset((
|
||||
specialElements = frozenset([
|
||||
(namespaces["html"], "address"),
|
||||
(namespaces["html"], "applet"),
|
||||
(namespaces["html"], "area"),
|
||||
@@ -416,22 +414,22 @@ specialElements = frozenset((
|
||||
(namespaces["html"], "wbr"),
|
||||
(namespaces["html"], "xmp"),
|
||||
(namespaces["svg"], "foreignObject")
|
||||
))
|
||||
])
|
||||
|
||||
htmlIntegrationPointElements = frozenset((
|
||||
htmlIntegrationPointElements = frozenset([
|
||||
(namespaces["mathml"], "annotaion-xml"),
|
||||
(namespaces["svg"], "foreignObject"),
|
||||
(namespaces["svg"], "desc"),
|
||||
(namespaces["svg"], "title")
|
||||
))
|
||||
])
|
||||
|
||||
mathmlTextIntegrationPointElements = frozenset((
|
||||
mathmlTextIntegrationPointElements = frozenset([
|
||||
(namespaces["mathml"], "mi"),
|
||||
(namespaces["mathml"], "mo"),
|
||||
(namespaces["mathml"], "mn"),
|
||||
(namespaces["mathml"], "ms"),
|
||||
(namespaces["mathml"], "mtext")
|
||||
))
|
||||
])
|
||||
|
||||
adjustForeignAttributes = {
|
||||
"xlink:actuate": ("xlink", "actuate", namespaces["xlink"]),
|
||||
@@ -451,21 +449,21 @@ adjustForeignAttributes = {
|
||||
unadjustForeignAttributes = dict([((ns, local), qname) for qname, (prefix, local, ns) in
|
||||
adjustForeignAttributes.items()])
|
||||
|
||||
spaceCharacters = frozenset((
|
||||
spaceCharacters = frozenset([
|
||||
"\t",
|
||||
"\n",
|
||||
"\u000C",
|
||||
" ",
|
||||
"\r"
|
||||
))
|
||||
])
|
||||
|
||||
tableInsertModeElements = frozenset((
|
||||
tableInsertModeElements = frozenset([
|
||||
"table",
|
||||
"tbody",
|
||||
"tfoot",
|
||||
"thead",
|
||||
"tr"
|
||||
))
|
||||
])
|
||||
|
||||
asciiLowercase = frozenset(string.ascii_lowercase)
|
||||
asciiUppercase = frozenset(string.ascii_uppercase)
|
||||
@@ -486,7 +484,7 @@ headingElements = (
|
||||
"h6"
|
||||
)
|
||||
|
||||
voidElements = frozenset((
|
||||
voidElements = frozenset([
|
||||
"base",
|
||||
"command",
|
||||
"event-source",
|
||||
@@ -502,11 +500,11 @@ voidElements = frozenset((
|
||||
"input",
|
||||
"source",
|
||||
"track"
|
||||
))
|
||||
])
|
||||
|
||||
cdataElements = frozenset(('title', 'textarea'))
|
||||
cdataElements = frozenset(['title', 'textarea'])
|
||||
|
||||
rcdataElements = frozenset((
|
||||
rcdataElements = frozenset([
|
||||
'style',
|
||||
'script',
|
||||
'xmp',
|
||||
@@ -514,27 +512,27 @@ rcdataElements = frozenset((
|
||||
'noembed',
|
||||
'noframes',
|
||||
'noscript'
|
||||
))
|
||||
])
|
||||
|
||||
booleanAttributes = {
|
||||
"": frozenset(("irrelevant",)),
|
||||
"style": frozenset(("scoped",)),
|
||||
"img": frozenset(("ismap",)),
|
||||
"audio": frozenset(("autoplay", "controls")),
|
||||
"video": frozenset(("autoplay", "controls")),
|
||||
"script": frozenset(("defer", "async")),
|
||||
"details": frozenset(("open",)),
|
||||
"datagrid": frozenset(("multiple", "disabled")),
|
||||
"command": frozenset(("hidden", "disabled", "checked", "default")),
|
||||
"hr": frozenset(("noshade")),
|
||||
"menu": frozenset(("autosubmit",)),
|
||||
"fieldset": frozenset(("disabled", "readonly")),
|
||||
"option": frozenset(("disabled", "readonly", "selected")),
|
||||
"optgroup": frozenset(("disabled", "readonly")),
|
||||
"button": frozenset(("disabled", "autofocus")),
|
||||
"input": frozenset(("disabled", "readonly", "required", "autofocus", "checked", "ismap")),
|
||||
"select": frozenset(("disabled", "readonly", "autofocus", "multiple")),
|
||||
"output": frozenset(("disabled", "readonly")),
|
||||
"": frozenset(["irrelevant"]),
|
||||
"style": frozenset(["scoped"]),
|
||||
"img": frozenset(["ismap"]),
|
||||
"audio": frozenset(["autoplay", "controls"]),
|
||||
"video": frozenset(["autoplay", "controls"]),
|
||||
"script": frozenset(["defer", "async"]),
|
||||
"details": frozenset(["open"]),
|
||||
"datagrid": frozenset(["multiple", "disabled"]),
|
||||
"command": frozenset(["hidden", "disabled", "checked", "default"]),
|
||||
"hr": frozenset(["noshade"]),
|
||||
"menu": frozenset(["autosubmit"]),
|
||||
"fieldset": frozenset(["disabled", "readonly"]),
|
||||
"option": frozenset(["disabled", "readonly", "selected"]),
|
||||
"optgroup": frozenset(["disabled", "readonly"]),
|
||||
"button": frozenset(["disabled", "autofocus"]),
|
||||
"input": frozenset(["disabled", "readonly", "required", "autofocus", "checked", "ismap"]),
|
||||
"select": frozenset(["disabled", "readonly", "autofocus", "multiple"]),
|
||||
"output": frozenset(["disabled", "readonly"]),
|
||||
}
|
||||
|
||||
# entitiesWindows1252 has to be _ordered_ and needs to have an index. It
|
||||
@@ -574,7 +572,7 @@ entitiesWindows1252 = (
|
||||
376 # 0x9F 0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS
|
||||
)
|
||||
|
||||
xmlEntities = frozenset(('lt;', 'gt;', 'amp;', 'apos;', 'quot;'))
|
||||
xmlEntities = frozenset(['lt;', 'gt;', 'amp;', 'apos;', 'quot;'])
|
||||
|
||||
entities = {
|
||||
"AElig": "\xc6",
|
||||
@@ -3088,8 +3086,8 @@ tokenTypes = {
|
||||
"ParseError": 7
|
||||
}
|
||||
|
||||
tagTokenTypes = frozenset((tokenTypes["StartTag"], tokenTypes["EndTag"],
|
||||
tokenTypes["EmptyTag"]))
|
||||
tagTokenTypes = frozenset([tokenTypes["StartTag"], tokenTypes["EndTag"],
|
||||
tokenTypes["EmptyTag"]])
|
||||
|
||||
|
||||
prefixes = dict([(v, k) for k, v in namespaces.items()])
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from gettext import gettext
|
||||
_ = gettext
|
||||
|
||||
from . import _base
|
||||
from ..constants import cdataElements, rcdataElements, voidElements
|
||||
|
||||
@@ -23,24 +20,24 @@ class Filter(_base.Filter):
|
||||
if type in ("StartTag", "EmptyTag"):
|
||||
name = token["name"]
|
||||
if contentModelFlag != "PCDATA":
|
||||
raise LintError(_("StartTag not in PCDATA content model flag: %(tag)s") % {"tag": name})
|
||||
raise LintError("StartTag not in PCDATA content model flag: %(tag)s" % {"tag": name})
|
||||
if not isinstance(name, str):
|
||||
raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name})
|
||||
raise LintError("Tag name is not a string: %(tag)r" % {"tag": name})
|
||||
if not name:
|
||||
raise LintError(_("Empty tag name"))
|
||||
raise LintError("Empty tag name")
|
||||
if type == "StartTag" and name in voidElements:
|
||||
raise LintError(_("Void element reported as StartTag token: %(tag)s") % {"tag": name})
|
||||
raise LintError("Void element reported as StartTag token: %(tag)s" % {"tag": name})
|
||||
elif type == "EmptyTag" and name not in voidElements:
|
||||
raise LintError(_("Non-void element reported as EmptyTag token: %(tag)s") % {"tag": token["name"]})
|
||||
raise LintError("Non-void element reported as EmptyTag token: %(tag)s" % {"tag": token["name"]})
|
||||
if type == "StartTag":
|
||||
open_elements.append(name)
|
||||
for name, value in token["data"]:
|
||||
if not isinstance(name, str):
|
||||
raise LintError(_("Attribute name is not a string: %(name)r") % {"name": name})
|
||||
raise LintError("Attribute name is not a string: %(name)r" % {"name": name})
|
||||
if not name:
|
||||
raise LintError(_("Empty attribute name"))
|
||||
raise LintError("Empty attribute name")
|
||||
if not isinstance(value, str):
|
||||
raise LintError(_("Attribute value is not a string: %(value)r") % {"value": value})
|
||||
raise LintError("Attribute value is not a string: %(value)r" % {"value": value})
|
||||
if name in cdataElements:
|
||||
contentModelFlag = "CDATA"
|
||||
elif name in rcdataElements:
|
||||
@@ -51,43 +48,43 @@ class Filter(_base.Filter):
|
||||
elif type == "EndTag":
|
||||
name = token["name"]
|
||||
if not isinstance(name, str):
|
||||
raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name})
|
||||
raise LintError("Tag name is not a string: %(tag)r" % {"tag": name})
|
||||
if not name:
|
||||
raise LintError(_("Empty tag name"))
|
||||
raise LintError("Empty tag name")
|
||||
if name in voidElements:
|
||||
raise LintError(_("Void element reported as EndTag token: %(tag)s") % {"tag": name})
|
||||
raise LintError("Void element reported as EndTag token: %(tag)s" % {"tag": name})
|
||||
start_name = open_elements.pop()
|
||||
if start_name != name:
|
||||
raise LintError(_("EndTag (%(end)s) does not match StartTag (%(start)s)") % {"end": name, "start": start_name})
|
||||
raise LintError("EndTag (%(end)s) does not match StartTag (%(start)s)" % {"end": name, "start": start_name})
|
||||
contentModelFlag = "PCDATA"
|
||||
|
||||
elif type == "Comment":
|
||||
if contentModelFlag != "PCDATA":
|
||||
raise LintError(_("Comment not in PCDATA content model flag"))
|
||||
raise LintError("Comment not in PCDATA content model flag")
|
||||
|
||||
elif type in ("Characters", "SpaceCharacters"):
|
||||
data = token["data"]
|
||||
if not isinstance(data, str):
|
||||
raise LintError(_("Attribute name is not a string: %(name)r") % {"name": data})
|
||||
raise LintError("Attribute name is not a string: %(name)r" % {"name": data})
|
||||
if not data:
|
||||
raise LintError(_("%(type)s token with empty data") % {"type": type})
|
||||
raise LintError("%(type)s token with empty data" % {"type": type})
|
||||
if type == "SpaceCharacters":
|
||||
data = data.strip(spaceCharacters)
|
||||
if data:
|
||||
raise LintError(_("Non-space character(s) found in SpaceCharacters token: %(token)r") % {"token": data})
|
||||
raise LintError("Non-space character(s) found in SpaceCharacters token: %(token)r" % {"token": data})
|
||||
|
||||
elif type == "Doctype":
|
||||
name = token["name"]
|
||||
if contentModelFlag != "PCDATA":
|
||||
raise LintError(_("Doctype not in PCDATA content model flag: %(name)s") % {"name": name})
|
||||
raise LintError("Doctype not in PCDATA content model flag: %(name)s" % {"name": name})
|
||||
if not isinstance(name, str):
|
||||
raise LintError(_("Tag name is not a string: %(tag)r") % {"tag": name})
|
||||
raise LintError("Tag name is not a string: %(tag)r" % {"tag": name})
|
||||
# XXX: what to do with token["data"] ?
|
||||
|
||||
elif type in ("ParseError", "SerializeError"):
|
||||
pass
|
||||
|
||||
else:
|
||||
raise LintError(_("Unknown token type: %(type)s") % {"type": type})
|
||||
raise LintError("Unknown token type: %(type)s" % {"type": type})
|
||||
|
||||
yield token
|
||||
|
||||
@@ -18,6 +18,7 @@ from .constants import cdataElements, rcdataElements
|
||||
from .constants import tokenTypes, ReparseException, namespaces
|
||||
from .constants import htmlIntegrationPointElements, mathmlTextIntegrationPointElements
|
||||
from .constants import adjustForeignAttributes as adjustForeignAttributesMap
|
||||
from .constants import E
|
||||
|
||||
|
||||
def parse(doc, treebuilder="etree", encoding=None,
|
||||
@@ -129,6 +130,17 @@ class HTMLParser(object):
|
||||
|
||||
self.framesetOK = True
|
||||
|
||||
@property
|
||||
def documentEncoding(self):
|
||||
"""The name of the character encoding
|
||||
that was used to decode the input stream,
|
||||
or :obj:`None` if that is not determined yet.
|
||||
|
||||
"""
|
||||
if not hasattr(self, 'tokenizer'):
|
||||
return None
|
||||
return self.tokenizer.stream.charEncoding[0]
|
||||
|
||||
def isHTMLIntegrationPoint(self, element):
|
||||
if (element.name == "annotation-xml" and
|
||||
element.namespace == namespaces["mathml"]):
|
||||
@@ -245,7 +257,7 @@ class HTMLParser(object):
|
||||
# XXX The idea is to make errorcode mandatory.
|
||||
self.errors.append((self.tokenizer.stream.position(), errorcode, datavars))
|
||||
if self.strict:
|
||||
raise ParseError
|
||||
raise ParseError(E[errorcode] % datavars)
|
||||
|
||||
def normalizeToken(self, token):
|
||||
""" HTML5 specific normalizations to the token stream """
|
||||
@@ -868,7 +880,7 @@ def getPhases(debug):
|
||||
self.startTagHandler = utils.MethodDispatcher([
|
||||
("html", self.startTagHtml),
|
||||
(("base", "basefont", "bgsound", "command", "link", "meta",
|
||||
"noframes", "script", "style", "title"),
|
||||
"script", "style", "title"),
|
||||
self.startTagProcessInHead),
|
||||
("body", self.startTagBody),
|
||||
("frameset", self.startTagFrameset),
|
||||
@@ -1205,8 +1217,7 @@ def getPhases(debug):
|
||||
attributes["name"] = "isindex"
|
||||
self.processStartTag(impliedTagToken("input", "StartTag",
|
||||
attributes=attributes,
|
||||
selfClosing=
|
||||
token["selfClosing"]))
|
||||
selfClosing=token["selfClosing"]))
|
||||
self.processEndTag(impliedTagToken("label"))
|
||||
self.processStartTag(impliedTagToken("hr", "StartTag"))
|
||||
self.processEndTag(impliedTagToken("form"))
|
||||
|
||||
@@ -28,7 +28,18 @@ asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters])
|
||||
asciiUppercaseBytes = frozenset([item.encode("ascii") for item in asciiUppercase])
|
||||
spacesAngleBrackets = spaceCharactersBytes | frozenset([b">", b"<"])
|
||||
|
||||
invalid_unicode_re = re.compile("[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uD800-\uDFFF\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]")
|
||||
|
||||
invalid_unicode_no_surrogate = "[\u0001-\u0008\u000B\u000E-\u001F\u007F-\u009F\uFDD0-\uFDEF\uFFFE\uFFFF\U0001FFFE\U0001FFFF\U0002FFFE\U0002FFFF\U0003FFFE\U0003FFFF\U0004FFFE\U0004FFFF\U0005FFFE\U0005FFFF\U0006FFFE\U0006FFFF\U0007FFFE\U0007FFFF\U0008FFFE\U0008FFFF\U0009FFFE\U0009FFFF\U000AFFFE\U000AFFFF\U000BFFFE\U000BFFFF\U000CFFFE\U000CFFFF\U000DFFFE\U000DFFFF\U000EFFFE\U000EFFFF\U000FFFFE\U000FFFFF\U0010FFFE\U0010FFFF]"
|
||||
|
||||
if utils.supports_lone_surrogates:
|
||||
# Use one extra step of indirection and create surrogates with
|
||||
# unichr. Not using this indirection would introduce an illegal
|
||||
# unicode literal on platforms not supporting such lone
|
||||
# surrogates.
|
||||
invalid_unicode_re = re.compile(invalid_unicode_no_surrogate +
|
||||
eval('"\\uD800-\\uDFFF"'))
|
||||
else:
|
||||
invalid_unicode_re = re.compile(invalid_unicode_no_surrogate)
|
||||
|
||||
non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE,
|
||||
0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF,
|
||||
@@ -164,13 +175,18 @@ class HTMLUnicodeInputStream(object):
|
||||
|
||||
"""
|
||||
|
||||
# Craziness
|
||||
if len("\U0010FFFF") == 1:
|
||||
if not utils.supports_lone_surrogates:
|
||||
# Such platforms will have already checked for such
|
||||
# surrogate errors, so no need to do this checking.
|
||||
self.reportCharacterErrors = None
|
||||
self.replaceCharactersRegexp = None
|
||||
elif len("\U0010FFFF") == 1:
|
||||
self.reportCharacterErrors = self.characterErrorsUCS4
|
||||
self.replaceCharactersRegexp = re.compile("[\uD800-\uDFFF]")
|
||||
self.replaceCharactersRegexp = re.compile(eval('"[\\uD800-\\uDFFF]"'))
|
||||
else:
|
||||
self.reportCharacterErrors = self.characterErrorsUCS2
|
||||
self.replaceCharactersRegexp = re.compile("([\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF])")
|
||||
self.replaceCharactersRegexp = re.compile(
|
||||
eval('"([\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF])"'))
|
||||
|
||||
# List of where new lines occur
|
||||
self.newLines = [0]
|
||||
@@ -265,11 +281,12 @@ class HTMLUnicodeInputStream(object):
|
||||
self._bufferedCharacter = data[-1]
|
||||
data = data[:-1]
|
||||
|
||||
self.reportCharacterErrors(data)
|
||||
if self.reportCharacterErrors:
|
||||
self.reportCharacterErrors(data)
|
||||
|
||||
# Replace invalid characters
|
||||
# Note U+0000 is dealt with in the tokenizer
|
||||
data = self.replaceCharactersRegexp.sub("\ufffd", data)
|
||||
# Replace invalid characters
|
||||
# Note U+0000 is dealt with in the tokenizer
|
||||
data = self.replaceCharactersRegexp.sub("\ufffd", data)
|
||||
|
||||
data = data.replace("\r\n", "\n")
|
||||
data = data.replace("\r", "\n")
|
||||
|
||||
@@ -2,11 +2,26 @@ from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import re
|
||||
from xml.sax.saxutils import escape, unescape
|
||||
from six.moves import urllib_parse as urlparse
|
||||
|
||||
from .tokenizer import HTMLTokenizer
|
||||
from .constants import tokenTypes
|
||||
|
||||
|
||||
content_type_rgx = re.compile(r'''
|
||||
^
|
||||
# Match a content type <application>/<type>
|
||||
(?P<content_type>[-a-zA-Z0-9.]+/[-a-zA-Z0-9.]+)
|
||||
# Match any character set and encoding
|
||||
(?:(?:;charset=(?:[-a-zA-Z0-9]+)(?:;(?:base64))?)
|
||||
|(?:;(?:base64))?(?:;charset=(?:[-a-zA-Z0-9]+))?)
|
||||
# Assume the rest is data
|
||||
,.*
|
||||
$
|
||||
''',
|
||||
re.VERBOSE)
|
||||
|
||||
|
||||
class HTMLSanitizerMixin(object):
|
||||
""" sanitization of XHTML+MathML+SVG and of inline style attributes."""
|
||||
|
||||
@@ -100,8 +115,8 @@ class HTMLSanitizerMixin(object):
|
||||
'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y',
|
||||
'y1', 'y2', 'zoomAndPan']
|
||||
|
||||
attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', 'poster',
|
||||
'xlink:href', 'xml:base']
|
||||
attr_val_is_uri = ['href', 'src', 'cite', 'action', 'longdesc', 'poster', 'background', 'datasrc',
|
||||
'dynsrc', 'lowsrc', 'ping', 'poster', 'xlink:href', 'xml:base']
|
||||
|
||||
svg_attr_val_allows_ref = ['clip-path', 'color-profile', 'cursor', 'fill',
|
||||
'filter', 'marker', 'marker-start', 'marker-mid', 'marker-end',
|
||||
@@ -138,7 +153,9 @@ class HTMLSanitizerMixin(object):
|
||||
acceptable_protocols = ['ed2k', 'ftp', 'http', 'https', 'irc',
|
||||
'mailto', 'news', 'gopher', 'nntp', 'telnet', 'webcal',
|
||||
'xmpp', 'callto', 'feed', 'urn', 'aim', 'rsync', 'tag',
|
||||
'ssh', 'sftp', 'rtsp', 'afs']
|
||||
'ssh', 'sftp', 'rtsp', 'afs', 'data']
|
||||
|
||||
acceptable_content_types = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/bmp', 'text/plain']
|
||||
|
||||
# subclasses may define their own versions of these constants
|
||||
allowed_elements = acceptable_elements + mathml_elements + svg_elements
|
||||
@@ -147,6 +164,7 @@ class HTMLSanitizerMixin(object):
|
||||
allowed_css_keywords = acceptable_css_keywords
|
||||
allowed_svg_properties = acceptable_svg_properties
|
||||
allowed_protocols = acceptable_protocols
|
||||
allowed_content_types = acceptable_content_types
|
||||
|
||||
# Sanitize the +html+, escaping all elements not in ALLOWED_ELEMENTS, and
|
||||
# stripping out all # attributes not in ALLOWED_ATTRIBUTES. Style
|
||||
@@ -189,10 +207,17 @@ class HTMLSanitizerMixin(object):
|
||||
unescape(attrs[attr])).lower()
|
||||
# remove replacement characters from unescaped characters
|
||||
val_unescaped = val_unescaped.replace("\ufffd", "")
|
||||
if (re.match("^[a-z0-9][-+.a-z0-9]*:", val_unescaped) and
|
||||
(val_unescaped.split(':')[0] not in
|
||||
self.allowed_protocols)):
|
||||
del attrs[attr]
|
||||
uri = urlparse.urlparse(val_unescaped)
|
||||
if uri and uri.scheme:
|
||||
if uri.scheme not in self.allowed_protocols:
|
||||
del attrs[attr]
|
||||
if uri.scheme == 'data':
|
||||
m = content_type_rgx.match(uri.path)
|
||||
if not m:
|
||||
del attrs[attr]
|
||||
elif m.group('content_type') not in self.allowed_content_types:
|
||||
del attrs[attr]
|
||||
|
||||
for attr in self.svg_attr_val_allows_ref:
|
||||
if attr in attrs:
|
||||
attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)',
|
||||
@@ -245,7 +270,7 @@ class HTMLSanitizerMixin(object):
|
||||
elif prop.split('-')[0].lower() in ['background', 'border', 'margin',
|
||||
'padding']:
|
||||
for keyword in value.split():
|
||||
if not keyword in self.acceptable_css_keywords and \
|
||||
if keyword not in self.acceptable_css_keywords and \
|
||||
not re.match("^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword):
|
||||
break
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from six import text_type
|
||||
|
||||
import gettext
|
||||
_ = gettext.gettext
|
||||
|
||||
try:
|
||||
from functools import reduce
|
||||
except ImportError:
|
||||
@@ -35,7 +32,7 @@ else:
|
||||
v = utils.surrogatePairToCodepoint(v)
|
||||
else:
|
||||
v = ord(v)
|
||||
if not v in encode_entity_map or k.islower():
|
||||
if v not in encode_entity_map or k.islower():
|
||||
# prefer < over < and similarly for &, >, etc.
|
||||
encode_entity_map[v] = k
|
||||
|
||||
@@ -208,7 +205,7 @@ class HTMLSerializer(object):
|
||||
if token["systemId"]:
|
||||
if token["systemId"].find('"') >= 0:
|
||||
if token["systemId"].find("'") >= 0:
|
||||
self.serializeError(_("System identifer contains both single and double quote characters"))
|
||||
self.serializeError("System identifer contains both single and double quote characters")
|
||||
quote_char = "'"
|
||||
else:
|
||||
quote_char = '"'
|
||||
@@ -220,7 +217,7 @@ class HTMLSerializer(object):
|
||||
elif type in ("Characters", "SpaceCharacters"):
|
||||
if type == "SpaceCharacters" or in_cdata:
|
||||
if in_cdata and token["data"].find("</") >= 0:
|
||||
self.serializeError(_("Unexpected </ in CDATA"))
|
||||
self.serializeError("Unexpected </ in CDATA")
|
||||
yield self.encode(token["data"])
|
||||
else:
|
||||
yield self.encode(escape(token["data"]))
|
||||
@@ -231,7 +228,7 @@ class HTMLSerializer(object):
|
||||
if name in rcdataElements and not self.escape_rcdata:
|
||||
in_cdata = True
|
||||
elif in_cdata:
|
||||
self.serializeError(_("Unexpected child element of a CDATA element"))
|
||||
self.serializeError("Unexpected child element of a CDATA element")
|
||||
for (attr_namespace, attr_name), attr_value in token["data"].items():
|
||||
# TODO: Add namespace support here
|
||||
k = attr_name
|
||||
@@ -279,20 +276,20 @@ class HTMLSerializer(object):
|
||||
if name in rcdataElements:
|
||||
in_cdata = False
|
||||
elif in_cdata:
|
||||
self.serializeError(_("Unexpected child element of a CDATA element"))
|
||||
self.serializeError("Unexpected child element of a CDATA element")
|
||||
yield self.encodeStrict("</%s>" % name)
|
||||
|
||||
elif type == "Comment":
|
||||
data = token["data"]
|
||||
if data.find("--") >= 0:
|
||||
self.serializeError(_("Comment contains --"))
|
||||
self.serializeError("Comment contains --")
|
||||
yield self.encodeStrict("<!--%s-->" % token["data"])
|
||||
|
||||
elif type == "Entity":
|
||||
name = token["name"]
|
||||
key = name + ";"
|
||||
if not key in entities:
|
||||
self.serializeError(_("Entity %s not recognized" % name))
|
||||
if key not in entities:
|
||||
self.serializeError("Entity %s not recognized" % name)
|
||||
if self.resolve_entities and key not in xmlEntities:
|
||||
data = entities[key]
|
||||
else:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user