diff --git a/CHANGELOG.md b/CHANGELOG.md
index af4f670e..41f2eeed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
# Changelog
+## v2.1.42 (2020-01-04)
+
+* Other:
+ * Fix: SSL certificate error when installing GeoLite2 database.
+ * Change: Verify MaxMind license key and GeoLite2 database path before installing.
+ * Change: Disable GeoLite2 database uninstall button when it is not installed.
+
+
+## v2.1.41 (2019-12-30)
+
+* Other:
+ * Fix: Failing to extract the GeoLite2 database on Windows.
+
+
+## v2.1.40 (2019-12-30)
+
+* UI:
+ * Change: Moved 3rd Party API settings to new tab in the settings.
+* Graphs:
+ * Change: Improve calculating month ranges for Play Totals graphs.
+* Other:
+ * Fix: Failing to verify a Plex Media Server using a hostname.
+ * Change: A license key is now required to install the MaxMind GeoLite2 database for IP geolocation. Please follow the guide in the wiki to reinstall the GeoLite2 database.
+ * Change: The GeoLite2 database will now automatically update periodically if installed.
+
+
## v2.1.39 (2019-12-08)
* UI:
diff --git a/data/interfaces/default/configuration_table.html b/data/interfaces/default/configuration_table.html
index ed0fa2c3..989b1766 100644
--- a/data/interfaces/default/configuration_table.html
+++ b/data/interfaces/default/configuration_table.html
@@ -53,14 +53,6 @@ DOCUMENTATION :: END
Newsletter Directory: |
${plexpy.CONFIG.NEWSLETTER_DIR} |
-
- GeoLite2 Database: |
- % if plexpy.CONFIG.GEOIP_DB:
- ${plexpy.CONFIG.GEOIP_DB} | Reinstall / Update | Uninstall |
- % else:
- Click here to install the GeoLite2 database. |
- % endif
-
% if plexpy.ARGS:
Arguments: |
@@ -102,22 +94,6 @@ DOCUMENTATION :: END
%def>
diff --git a/data/interfaces/default/stream_data.html b/data/interfaces/default/stream_data.html
index a6e9099e..3165723c 100644
--- a/data/interfaces/default/stream_data.html
+++ b/data/interfaces/default/stream_data.html
@@ -178,6 +178,11 @@ DOCUMENTATION :: END
${data['stream_video_framerate']} |
${data['video_framerate']} |
+
+ Dynamic Range |
+ ${data['stream_video_dynamic_range']} |
+ ${data['video_dynamic_range']} |
+
Aspect Ratio |
- |
diff --git a/lib/certifi/__init__.py b/lib/certifi/__init__.py
index 632db8e1..0d59a056 100644
--- a/lib/certifi/__init__.py
+++ b/lib/certifi/__init__.py
@@ -1,3 +1,3 @@
from .core import where
-__version__ = "2019.03.09"
+__version__ = "2019.11.28"
diff --git a/lib/certifi/cacert.pem b/lib/certifi/cacert.pem
index 84636dde..a4758ef3 100644
--- a/lib/certifi/cacert.pem
+++ b/lib/certifi/cacert.pem
@@ -771,36 +771,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
+OkuE6N36B9K
-----END CERTIFICATE-----
-# Issuer: CN=Class 2 Primary CA O=Certplus
-# Subject: CN=Class 2 Primary CA O=Certplus
-# Label: "Certplus Class 2 Primary CA"
-# Serial: 177770208045934040241468760488327595043
-# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b
-# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb
-# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb
------BEGIN CERTIFICATE-----
-MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw
-PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz
-cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9
-MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz
-IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ
-ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR
-VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL
-kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd
-EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas
-H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0
-HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud
-DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4
-QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu
-Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/
-AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8
-yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR
-FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA
-ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB
-kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
-l7+ijrRU
------END CERTIFICATE-----
-
# Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co.
# Subject: CN=DST Root CA X3 O=Digital Signature Trust Co.
# Label: "DST Root CA X3"
@@ -1219,36 +1189,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
-----END CERTIFICATE-----
-# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
-# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center
-# Label: "Deutsche Telekom Root CA 2"
-# Serial: 38
-# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08
-# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf
-# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3
------BEGIN CERTIFICATE-----
-MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
-MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
-IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
-IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
-RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
-U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
-IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
-ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
-QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
-rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
-NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
-QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
-txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
-BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
-AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
-tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
-IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
-6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
-xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
-Cm26OWMohpLzGITY+9HPBVZkVw==
------END CERTIFICATE-----
-
# Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc
# Subject: CN=Cybertrust Global Root O=Cybertrust, Inc
# Label: "Cybertrust Global Root"
@@ -3453,46 +3393,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
-----END CERTIFICATE-----
-# Issuer: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
-# Subject: CN=Certinomis - Root CA O=Certinomis OU=0002 433998903
-# Label: "Certinomis - Root CA"
-# Serial: 1
-# MD5 Fingerprint: 14:0a:fd:8d:a8:28:b5:38:69:db:56:7e:61:22:03:3f
-# SHA1 Fingerprint: 9d:70:bb:01:a5:a4:a0:18:11:2e:f7:1c:01:b9:32:c5:34:e7:88:a8
-# SHA256 Fingerprint: 2a:99:f5:bc:11:74:b7:3c:bb:1d:62:08:84:e0:1c:34:e5:1c:cb:39:78:da:12:5f:0e:33:26:88:83:bf:41:58
------BEGIN CERTIFICATE-----
-MIIFkjCCA3qgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJGUjET
-MBEGA1UEChMKQ2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxHTAb
-BgNVBAMTFENlcnRpbm9taXMgLSBSb290IENBMB4XDTEzMTAyMTA5MTcxOFoXDTMz
-MTAyMTA5MTcxOFowWjELMAkGA1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMx
-FzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMR0wGwYDVQQDExRDZXJ0aW5vbWlzIC0g
-Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTMCQosP5L2
-fxSeC5yaah1AMGT9qt8OHgZbn1CF6s2Nq0Nn3rD6foCWnoR4kkjW4znuzuRZWJfl
-LieY6pOod5tK8O90gC3rMB+12ceAnGInkYjwSond3IjmFPnVAy//ldu9n+ws+hQV
-WZUKxkd8aRi5pwP5ynapz8dvtF4F/u7BUrJ1Mofs7SlmO/NKFoL21prbcpjp3vDF
-TKWrteoB4owuZH9kb/2jJZOLyKIOSY008B/sWEUuNKqEUL3nskoTuLAPrjhdsKkb
-5nPJWqHZZkCqqU2mNAKthH6yI8H7KsZn9DS2sJVqM09xRLWtwHkziOC/7aOgFLSc
-CbAK42C++PhmiM1b8XcF4LVzbsF9Ri6OSyemzTUK/eVNfaoqoynHWmgE6OXWk6Ri
-wsXm9E/G+Z8ajYJJGYrKWUM66A0ywfRMEwNvbqY/kXPLynNvEiCL7sCCeN5LLsJJ
-wx3tFvYk9CcbXFcx3FXuqB5vbKziRcxXV4p1VxngtViZSTYxPDMBbRZKzbgqg4SG
-m/lg0h9tkQPTYKbVPZrdd5A9NaSfD171UkRpucC63M9933zZxKyGIjK8e2uR73r4
-F2iw4lNVYC2vPsKD2NkJK/DAZNuHi5HMkesE/Xa0lZrmFAYb1TQdvtj/dBxThZng
-WVJKYe2InmtJiUZ+IFrZ50rlau7SZRFDAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIB
-BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTvkUz1pcMw6C8I6tNxIqSSaHh0
-2TAfBgNVHSMEGDAWgBTvkUz1pcMw6C8I6tNxIqSSaHh02TANBgkqhkiG9w0BAQsF
-AAOCAgEAfj1U2iJdGlg+O1QnurrMyOMaauo++RLrVl89UM7g6kgmJs95Vn6RHJk/
-0KGRHCwPT5iVWVO90CLYiF2cN/z7ZMF4jIuaYAnq1fohX9B0ZedQxb8uuQsLrbWw
-F6YSjNRieOpWauwK0kDDPAUwPk2Ut59KA9N9J0u2/kTO+hkzGm2kQtHdzMjI1xZS
-g081lLMSVX3l4kLr5JyTCcBMWwerx20RoFAXlCOotQqSD7J6wWAsOMwaplv/8gzj
-qh8c3LigkyfeY+N/IZ865Z764BNqdeuWXGKRlI5nU7aJ+BIJy29SWwNyhlCVCNSN
-h4YVH5Uk2KRvms6knZtt0rJ2BobGVgjF6wnaNsIbW0G+YSrjcOa4pvi2WsS9Iff/
-ql+hbHY5ZtbqTFXhADObE5hjyW/QASAJN1LnDE8+zbz1X5YnpyACleAu6AdBBR8V
-btaw5BngDwKTACdyxYvRVB9dSsNAl35VpnzBMwQUAR1JIGkLGZOdblgi90AMRgwj
-Y/M50n92Uaf0yKHxDHYiI0ZSKS3io0EHVmmY0gUJvGnHWmHNj4FgFU2A3ZDifcRQ
-8ow7bkrHxuaAKzyBvBGAFhAn1/DNP3nMcyrDflOR1m749fPH0FFNjkulW+YZFzvW
-gQncItzujrnEj1PhZ7szuIgVRs/taTX/dQ1G885x4cVrhkIGuUE=
------END CERTIFICATE-----
-
# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed
# Label: "OISTE WISeKey Global Root GB CA"
@@ -4656,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa
LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG
mpv0
-----END CERTIFICATE-----
+
+# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
+# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only
+# Label: "Entrust Root Certification Authority - G4"
+# Serial: 289383649854506086828220374796556676440
+# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88
+# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01
+# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88
+-----BEGIN CERTIFICATE-----
+MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw
+gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL
+Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg
+MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw
+BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0
+MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT
+MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1
+c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ
+bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg
+Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ
+2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E
+T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j
+5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM
+C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T
+DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX
+wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A
+2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm
+nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8
+dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl
+N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj
+c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
+VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS
+5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS
+Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr
+hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/
+B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI
+AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw
+H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+
+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk
+2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol
+IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk
+5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY
+n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw==
+-----END CERTIFICATE-----
diff --git a/plexpy/__init__.py b/plexpy/__init__.py
index 2ce2752e..2b2e555c 100644
--- a/plexpy/__init__.py
+++ b/plexpy/__init__.py
@@ -172,7 +172,7 @@ def initialize(config_file):
SYS_TIMEZONE.zone, SYS_UTC_OFFSET
))
logger.info("Python {}".format(
- sys.version
+ sys.version.replace('\n', '')
))
logger.info("Program Dir: {}".format(
PROG_DIR
@@ -450,6 +450,8 @@ def initialize_scheduler():
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
schedule_job(config.make_backup, 'Backup Tautulli config',
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
+ schedule_job(helpers.update_geoip_db, 'Update GeoLite2 database',
+ hours=12 * bool(CONFIG.GEOIP_DB_INSTALLED), minutes=0, seconds=0)
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
@@ -588,12 +590,14 @@ def dbcheck():
'view_offset INTEGER DEFAULT 0, duration INTEGER, video_decision TEXT, audio_decision TEXT, '
'transcode_decision TEXT, container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, '
'video_codec TEXT, video_bitrate INTEGER, video_resolution TEXT, video_width INTEGER, video_height INTEGER, '
- 'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
+ 'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, '
+ 'video_dynamic_range TEXT, aspect_ratio TEXT, '
'audio_codec TEXT, audio_bitrate INTEGER, audio_channels INTEGER, subtitle_codec TEXT, '
'stream_bitrate INTEGER, stream_video_resolution TEXT, quality_profile TEXT, '
'stream_container_decision TEXT, stream_container TEXT, '
'stream_video_decision TEXT, stream_video_codec TEXT, stream_video_bitrate INTEGER, stream_video_width INTEGER, '
'stream_video_height INTEGER, stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
+ 'stream_video_dynamic_range TEXT, '
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
'subtitles INTEGER, stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, '
'transcode_protocol TEXT, transcode_container TEXT, '
@@ -623,7 +627,7 @@ def dbcheck():
'video_decision TEXT, audio_decision TEXT, transcode_decision TEXT, duration INTEGER DEFAULT 0, '
'container TEXT, bitrate INTEGER, width INTEGER, height INTEGER, video_bitrate INTEGER, video_bit_depth INTEGER, '
'video_codec TEXT, video_codec_level TEXT, video_width INTEGER, video_height INTEGER, video_resolution TEXT, '
- 'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, aspect_ratio TEXT, '
+ 'video_framerate TEXT, video_scan_type TEXT, video_full_resolution TEXT, video_dynamic_range TEXT, aspect_ratio TEXT, '
'audio_bitrate INTEGER, audio_codec TEXT, audio_channels INTEGER, transcode_protocol TEXT, '
'transcode_container TEXT, transcode_video_codec TEXT, transcode_audio_codec TEXT, '
'transcode_audio_channels INTEGER, transcode_width INTEGER, transcode_height INTEGER, '
@@ -633,7 +637,7 @@ def dbcheck():
'stream_container TEXT, stream_container_decision TEXT, stream_bitrate INTEGER, '
'stream_video_decision TEXT, stream_video_bitrate INTEGER, stream_video_codec TEXT, stream_video_codec_level TEXT, '
'stream_video_bit_depth INTEGER, stream_video_height INTEGER, stream_video_width INTEGER, stream_video_resolution TEXT, '
- 'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, '
+ 'stream_video_framerate TEXT, stream_video_scan_type TEXT, stream_video_full_resolution TEXT, stream_video_dynamic_range TEXT, '
'stream_audio_decision TEXT, stream_audio_codec TEXT, stream_audio_bitrate INTEGER, stream_audio_channels INTEGER, '
'stream_subtitle_decision TEXT, stream_subtitle_codec TEXT, stream_subtitle_container TEXT, stream_subtitle_forced INTEGER, '
'subtitles INTEGER, subtitle_codec TEXT, synced_version INTEGER, synced_version_profile TEXT, '
@@ -1206,6 +1210,18 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN stream_video_full_resolution TEXT'
)
+ # Upgrade sessions table from earlier versions
+ try:
+ c_db.execute('SELECT video_dynamic_range FROM sessions')
+ except sqlite3.OperationalError:
+ logger.debug(u"Altering database. Updating database table sessions.")
+ c_db.execute(
+ 'ALTER TABLE sessions ADD COLUMN video_dynamic_range TEXT'
+ )
+ c_db.execute(
+ 'ALTER TABLE sessions ADD COLUMN stream_video_dynamic_range TEXT'
+ )
+
# Upgrade session_history table from earlier versions
try:
c_db.execute('SELECT reference_id FROM session_history')
@@ -1544,6 +1560,17 @@ def dbcheck():
'ELSE stream_video_resolution || "p" END)'
)
+ # Upgrade session_history_media_info table from earlier versions
+ try:
+ c_db.execute('SELECT video_dynamic_range FROM session_history_media_info')
+ except sqlite3.OperationalError:
+ logger.debug(u"Altering database. Updating database table session_history_media_info.")
+ c_db.execute(
+ 'ALTER TABLE session_history_media_info ADD COLUMN video_dynamic_range TEXT '
+ )
+ c_db.execute(
+ 'ALTER TABLE session_history_media_info ADD COLUMN stream_video_dynamic_range TEXT '
+ )
# Upgrade users table from earlier versions
try:
c_db.execute('SELECT do_notify FROM users')
diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py
index 601ab0d8..30ba7dc9 100644
--- a/plexpy/activity_processor.py
+++ b/plexpy/activity_processor.py
@@ -83,6 +83,7 @@ class ActivityProcessor(object):
'video_framerate': session.get('video_framerate', ''),
'video_scan_type': session.get('video_scan_type', ''),
'video_full_resolution': session.get('video_full_resolution', ''),
+ 'video_dynamic_range': session.get('video_dynamic_range', ''),
'aspect_ratio': session.get('aspect_ratio', ''),
'audio_codec': session.get('audio_codec', ''),
'audio_bitrate': session.get('audio_bitrate', ''),
@@ -115,6 +116,7 @@ class ActivityProcessor(object):
'stream_video_framerate': session.get('stream_video_framerate', ''),
'stream_video_scan_type': session.get('stream_video_scan_type', ''),
'stream_video_full_resolution': session.get('stream_video_full_resolution', ''),
+ 'stream_video_dynamic_range': session.get('stream_video_dynamic_range', ''),
'stream_audio_decision': session.get('stream_audio_decision', ''),
'stream_audio_codec': session.get('stream_audio_codec', ''),
'stream_audio_bitrate': session.get('stream_audio_bitrate', ''),
@@ -358,6 +360,7 @@ class ActivityProcessor(object):
'video_framerate': session['video_framerate'],
'video_scan_type': session['video_scan_type'],
'video_full_resolution': session['video_full_resolution'],
+ 'video_dynamic_range': session['video_dynamic_range'],
'aspect_ratio': session['aspect_ratio'],
'audio_codec': session['audio_codec'],
'audio_bitrate': session['audio_bitrate'],
@@ -392,6 +395,7 @@ class ActivityProcessor(object):
'stream_video_framerate': session['stream_video_framerate'],
'stream_video_scan_type': session['stream_video_scan_type'],
'stream_video_full_resolution': session['stream_video_full_resolution'],
+ 'stream_video_dynamic_range': session['stream_video_dynamic_range'],
'stream_audio_decision': session['stream_audio_decision'],
'stream_audio_codec': session['stream_audio_codec'],
'stream_audio_bitrate': session['stream_audio_bitrate'],
diff --git a/plexpy/common.py b/plexpy/common.py
index c4acd7e0..4030d523 100644
--- a/plexpy/common.py
+++ b/plexpy/common.py
@@ -61,7 +61,9 @@ PLATFORM_NAME_OVERRIDES = {
'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360',
- 'WebMAF': 'Playstation 4'
+ 'WebMAF': 'Playstation 4',
+ 'windows': 'Windows',
+ 'osx': 'macOS'
}
PMS_PLATFORM_NAME_OVERRIDES = {
@@ -204,7 +206,8 @@ SCHEDULER_LIST = [
'Refresh libraries list',
'Refresh Plex server URLs',
'Backup Tautulli database',
- 'Backup Tautulli config'
+ 'Backup Tautulli config',
+ 'Update GeoLite2 database'
]
DATE_TIME_FORMATS = [
@@ -367,6 +370,12 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Stream Video Codec Level', 'type': 'int', 'value': 'stream_video_codec_level', 'description': 'The video codec level of the stream.'},
{'name': 'Stream Video Bitrate', 'type': 'int', 'value': 'stream_video_bitrate', 'description': 'The video bitrate (in kbps) of the stream.'},
{'name': 'Stream Video Bit Depth', 'type': 'int', 'value': 'stream_video_bit_depth', 'description': 'The video bit depth of the stream.'},
+ {'name': 'Stream Video Chroma Subsampling', 'type': 'str', 'value': 'stream_video_chroma_subsampling', 'description': 'The video chroma subsampling of the stream.'},
+ {'name': 'Stream Video Color Primaries', 'type': 'srt', 'value': 'stream_video_color_primaries', 'description': 'The video color primaries of the stream.'},
+ {'name': 'Stream Video Color Range', 'type': 'srt', 'value': 'stream_video_color_range', 'description': 'The video color range of the stream.'},
+ {'name': 'Stream Video Color Space', 'type': 'str', 'value': 'stream_video_color_space', 'description': 'The video color space of the stream.'},
+ {'name': 'Stream Video Color Transfer Function', 'type': 'str', 'value': 'stream_video_color_trc', 'description': 'The video transfer function of the stream.'},
+ {'name': 'Stream Video Dynamic Range', 'type': 'str', 'value': 'stream_video_dynamic_range', 'description': 'The video dynamic range of the stream.', 'example': 'HDR or SDR'},
{'name': 'Stream Video Framerate', 'type': 'str', 'value': 'stream_video_framerate', 'description': 'The video framerate of the stream.'},
{'name': 'Stream Video Full Resolution', 'type': 'str', 'value': 'stream_video_full_resolution', 'description': 'The video resolution of the stream with scan type.'},
{'name': 'Stream Video Ref Frames', 'type': 'int', 'value': 'stream_video_ref_frames', 'description': 'The video reference frames of the stream.'},
@@ -474,6 +483,12 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Video Codec Level', 'type': 'int', 'value': 'video_codec_level', 'description': 'The video codec level of the original media.'},
{'name': 'Video Bitrate', 'type': 'int', 'value': 'video_bitrate', 'description': 'The video bitrate of the original media.'},
{'name': 'Video Bit Depth', 'type': 'int', 'value': 'video_bit_depth', 'description': 'The video bit depth of the original media.'},
+ {'name': 'Video Chroma Subsampling', 'type': 'str', 'value': 'video_chroma_subsampling', 'description': 'The video chroma subsampling of the original media.'},
+ {'name': 'Video Color Primaries', 'type': 'srt', 'value': 'video_color_primaries', 'description': 'The video color primaries of the original media.'},
+ {'name': 'Video Color Range', 'type': 'srt', 'value': 'video_color_range', 'description': 'The video color range of the original media.'},
+ {'name': 'Video Color Space', 'type': 'str', 'value': 'video_color_space', 'description': 'The video color space of the original media.'},
+ {'name': 'Video Color Transfer Function', 'type': 'str', 'value': 'video_color_trc', 'description': 'The video transfer function of the original media.'},
+ {'name': 'Video Dynamic Range', 'type': 'str', 'value': 'video_dynamic_range', 'description': 'The video dynamic range of the original media.', 'example': 'HDR or SDR'},
{'name': 'Video Framerate', 'type': 'str', 'value': 'video_framerate', 'description': 'The video framerate of the original media.'},
{'name': 'Video Full Resolution', 'type': 'str', 'value': 'video_full_resolution', 'description': 'The video resolution of the original media with scan type.'},
{'name': 'Video Ref Frames', 'type': 'int', 'value': 'video_ref_frames', 'description': 'The video reference frames of the original media.'},
diff --git a/plexpy/config.py b/plexpy/config.py
index 59a9d332..d5d79c6b 100644
--- a/plexpy/config.py
+++ b/plexpy/config.py
@@ -180,6 +180,8 @@ _CONFIG_DEFINITIONS = {
'FIRST_RUN_COMPLETE': (int, 'General', 0),
'FREEZE_DB': (int, 'General', 0),
'GEOIP_DB': (str, 'General', ''),
+ 'GEOIP_DB_INSTALLED': (int, 'General', 0),
+ 'GEOIP_DB_UPDATE_DAYS': (int, 'General', 30),
'GET_FILE_SIZES': (int, 'General', 0),
'GET_FILE_SIZES_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
'GIT_BRANCH': (str, 'General', 'master'),
@@ -294,6 +296,7 @@ _CONFIG_DEFINITIONS = {
'LOG_BLACKLIST': (int, 'General', 1),
'LOG_DIR': (str, 'General', ''),
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
+ 'MAXMIND_LICENSE_KEY': (str, 'General', ''),
'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
@@ -934,3 +937,9 @@ class Config(object):
self.BUFFER_THRESHOLD = max(self.BUFFER_THRESHOLD, 10)
self.CONFIG_VERSION = 13
+
+ if self.CONFIG_VERSION == 13:
+ if not self.GEOIP_DB:
+ self.GEOIP_DB = os.path.join(plexpy.DATA_DIR, 'GeoLite2-City.mmdb')
+
+ self.CONFIG_VERSION = 14
diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py
index 71cd0007..23cb39fc 100644
--- a/plexpy/datafactory.py
+++ b/plexpy/datafactory.py
@@ -888,11 +888,12 @@ class DataFactory(object):
query = 'SELECT bitrate, video_full_resolution, ' \
'optimized_version, optimized_version_profile, optimized_version_title, ' \
'synced_version, synced_version_profile, ' \
- 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
+ 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, ' \
+ 'video_dynamic_range, aspect_ratio, ' \
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
- 'stream_video_framerate, ' \
+ 'stream_video_framerate, stream_video_dynamic_range, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
@@ -909,11 +910,12 @@ class DataFactory(object):
query = 'SELECT bitrate, video_full_resolution, ' \
'optimized_version, optimized_version_profile, optimized_version_title, ' \
'synced_version, synced_version_profile, ' \
- 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, aspect_ratio, ' \
+ 'container, video_codec, video_bitrate, video_width, video_height, video_framerate, ' \
+ 'video_dynamic_range, aspect_ratio, ' \
'audio_codec, audio_bitrate, audio_channels, subtitle_codec, ' \
'stream_bitrate, stream_video_full_resolution, quality_profile, stream_container_decision, stream_container, ' \
'stream_video_decision, stream_video_codec, stream_video_bitrate, stream_video_width, stream_video_height, ' \
- 'stream_video_framerate, ' \
+ 'stream_video_framerate, stream_video_dynamic_range, ' \
'stream_audio_decision, stream_audio_codec, stream_audio_bitrate, stream_audio_channels, ' \
'subtitles, stream_subtitle_decision, stream_subtitle_codec, ' \
'transcode_hw_decoding, transcode_hw_encoding, ' \
@@ -960,6 +962,7 @@ class DataFactory(object):
'video_width': item['video_width'],
'video_height': item['video_height'],
'video_framerate': item['video_framerate'],
+ 'video_dynamic_range': item['video_dynamic_range'],
'aspect_ratio': item['aspect_ratio'],
'audio_codec': item['audio_codec'],
'audio_bitrate': item['audio_bitrate'],
@@ -976,6 +979,7 @@ class DataFactory(object):
'stream_video_width': item['stream_video_width'],
'stream_video_height': item['stream_video_height'],
'stream_video_framerate': item['stream_video_framerate'],
+ 'stream_video_dynamic_range': item['stream_video_dynamic_range'],
'stream_audio_decision': item['stream_audio_decision'],
'stream_audio_codec': item['stream_audio_codec'],
'stream_audio_bitrate': item['stream_audio_bitrate'],
diff --git a/plexpy/helpers.py b/plexpy/helpers.py
index 9d8a29df..f5e54e21 100644
--- a/plexpy/helpers.py
+++ b/plexpy/helpers.py
@@ -27,6 +27,7 @@ from past.builtins import basestring
from past.utils import old_div
import base64
+import certifi
import cloudinary
from cloudinary.api import delete_resources_by_tag
from cloudinary.uploader import upload
@@ -35,7 +36,6 @@ import datetime
from functools import wraps
import geoip2.database
import geoip2.errors
-import gzip
import hashlib
import imghdr
from itertools import zip_longest
@@ -50,16 +50,13 @@ from operator import itemgetter
import os
import re
import shlex
+import shutil
import socket
import sys
+import tarfile
import time
import unicodedata
-import urllib.request
-import urllib.parse
-import urllib.error
-import urllib.request
-import urllib.error
-import urllib.parse
+import urllib3
from xml.dom import minidom
import xmltodict
@@ -604,83 +601,127 @@ def is_valid_ip(address):
return False
-def install_geoip_db():
- maxmind_url = 'http://geolite.maxmind.com/download/geoip/database/'
- geolite2_gz = 'GeoLite2-City.mmdb.gz'
- geolite2_md5 = 'GeoLite2-City.md5'
- geolite2_db = geolite2_gz[:-3]
- md5_checksum = ''
+def update_geoip_db():
+ if plexpy.CONFIG.GEOIP_DB_INSTALLED:
+ logger.info(u"Tautulli Helpers :: Checking for GeoLite2 database updates.")
+ now = int(time.time())
+ if now - plexpy.CONFIG.GEOIP_DB_INSTALLED >= plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS * 24 * 60 * 60:
+ return install_geoip_db(update=True)
+ logger.info(u"Tautulli Helpers :: GeoLite2 database already updated within the last %s days."
+ % plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS)
+
+
+def install_geoip_db(update=False):
+ if not plexpy.CONFIG.MAXMIND_LICENSE_KEY:
+ logger.error(u"Tautulli Helpers :: Failed to download GeoLite2 database file from MaxMind: Missing MaxMindLicense Key")
+ return False
+
+ maxmind_db = 'GeoLite2-City'
+ maxmind_url = 'https://download.maxmind.com/app/geoip_download?edition_id={db}&suffix={{suffix}}&license_key={key}'.format(
+ db=maxmind_db, key=plexpy.CONFIG.MAXMIND_LICENSE_KEY)
+ geolite2_db_url = maxmind_url.format(suffix='tar.gz')
+ geolite2_md5_url = maxmind_url.format(suffix='tar.gz.md5')
+ geolite2_gz = maxmind_db + '.tar.gz'
+ geolite2_md5 = geolite2_gz + '.md5'
+ geolite2_db = maxmind_db + '.mmdb'
+ geolite2_db_path = plexpy.CONFIG.GEOIP_DB or os.path.join(plexpy.DATA_DIR, geolite2_db)
+
+ # Check path ends with .mmdb
+ if os.path.splitext(geolite2_db_path)[1] != os.path.splitext(geolite2_db)[1]:
+ geolite2_db_path = os.path.join(geolite2_db_path, geolite2_db)
temp_gz = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_gz)
- geolite2_db = plexpy.CONFIG.GEOIP_DB or os.path.join(plexpy.DATA_DIR, geolite2_db)
+ temp_md5 = os.path.join(plexpy.CONFIG.CACHE_DIR, geolite2_md5)
# Retrieve the GeoLite2 gzip file
logger.debug("Tautulli Helpers :: Downloading GeoLite2 gzip file from MaxMind...")
try:
- maxmind = urllib.request.URLopener()
- maxmind.retrieve(maxmind_url + geolite2_gz, temp_gz)
- md5_checksum = urllib.request.urlopen(maxmind_url + geolite2_md5).read()
+ maxmind = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
+ with maxmind.request('GET', geolite2_db_url, preload_content=False) as r_db, open(temp_gz, 'wb') as f_db:
+ shutil.copyfileobj(r_db, f_db)
+ with maxmind.request('GET', geolite2_md5_url, preload_content=False) as r_md5, open(temp_md5, 'wb') as f_md5:
+ shutil.copyfileobj(r_md5, f_md5)
except Exception as e:
logger.error("Tautulli Helpers :: Failed to download GeoLite2 gzip file from MaxMind: %s" % e)
return False
- # Extract the GeoLite2 database file
- logger.debug("Tautulli Helpers :: Extracting GeoLite2 database...")
- try:
- with gzip.open(temp_gz, 'rb') as gz:
- with open(geolite2_db, 'wb') as db:
- db.write(gz.read())
- except Exception as e:
- logger.error("Tautulli Helpers :: Failed to extract the GeoLite2 database: %s" % e)
- return False
-
- # Check MD5 hash for GeoLite2 database file
- logger.debug("Tautulli Helpers :: Checking MD5 checksum for GeoLite2 database...")
+ # Check MD5 hash for GeoLite2 tar.gz file
+ logger.debug(u"Tautulli Helpers :: Checking MD5 checksum for GeoLite2 gzip file...")
try:
hash_md5 = hashlib.md5()
- with open(geolite2_db, 'rb') as f:
+ with open(temp_gz, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
md5_hash = hash_md5.hexdigest()
+ with open(temp_md5, 'r') as f:
+ md5_checksum = f.read()
+
if md5_hash != md5_checksum:
logger.error("Tautulli Helpers :: MD5 checksum doesn't match for GeoLite2 database. "
"Checksum: %s, file hash: %s" % (md5_checksum, md5_hash))
return False
except Exception as e:
- logger.error("Tautulli Helpers :: Failed to generate MD5 checksum for GeoLite2 database: %s" % e)
+ logger.error(u"Tautulli Helpers :: Failed to generate MD5 checksum for GeoLite2 gzip file: %s" % e)
+ return False
+
+ # Extract the GeoLite2 database file
+ logger.debug(u"Tautulli Helpers :: Extracting GeoLite2 database...")
+ try:
+ mmdb = None
+ with tarfile.open(temp_gz, 'r:gz') as tar:
+ for member in tar.getmembers():
+ if geolite2_db in member.name:
+ member.name = os.path.basename(member.name)
+ tar.extractall(path=os.path.dirname(geolite2_db_path), members=[member])
+ mmdb = True
+ break
+ if not mmdb:
+ raise Exception("{} not found in gzip file.".format(geolite2_db))
+ except Exception as e:
+ logger.error(u"Tautulli Helpers :: Failed to extract the GeoLite2 database: %s" % e)
return False
# Delete temportary GeoLite2 gzip file
logger.debug("Tautulli Helpers :: Deleting temporary GeoLite2 gzip file...")
try:
os.remove(temp_gz)
+ os.remove(temp_md5)
except Exception as e:
logger.warn("Tautulli Helpers :: Failed to remove temporary GeoLite2 gzip file: %s" % e)
- logger.debug("Tautulli Helpers :: GeoLite2 database installed successfully.")
- plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db)
+ plexpy.CONFIG.__setattr__('GEOIP_DB', geolite2_db_path)
+ plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', int(time.time()))
plexpy.CONFIG.write()
- return True
+ logger.debug(u"Tautulli Helpers :: GeoLite2 database installed successfully.")
+
+ if not update:
+ plexpy.schedule_job(update_geoip_db, 'Update GeoLite2 database', hours=12, minutes=0, seconds=0)
+
+ return plexpy.CONFIG.GEOIP_DB_INSTALLED
def uninstall_geoip_db():
logger.debug("Tautulli Helpers :: Uninstalling the GeoLite2 database...")
try:
os.remove(plexpy.CONFIG.GEOIP_DB)
- plexpy.CONFIG.__setattr__('GEOIP_DB', '')
- plexpy.CONFIG.write()
except Exception as e:
logger.error("Tautulli Helpers :: Failed to uninstall the GeoLite2 database: %s" % e)
return False
- logger.debug("Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
+ plexpy.CONFIG.__setattr__('GEOIP_DB_INSTALLED', 0)
+ plexpy.CONFIG.write()
+
+ logger.debug(u"Tautulli Helpers :: GeoLite2 database uninstalled successfully.")
+
+ plexpy.schedule_job(update_geoip_db, 'Update GeoLite2 database', hours=0, minutes=0, seconds=0)
+
return True
def geoip_lookup(ip_address):
- if not plexpy.CONFIG.GEOIP_DB:
+ if not plexpy.CONFIG.GEOIP_DB_INSTALLED:
return 'GeoLite2 database not installed. Please install from the ' \
'Settings page.'
@@ -698,7 +739,7 @@ def geoip_lookup(ip_address):
'Settings page.'
except maxminddb.InvalidDatabaseError as e:
return 'Invalid GeoLite2 database. Please reinstall from the ' \
- 'Settings page.'
+ 'Settings page.'
except geoip2.errors.AddressNotFoundError as e:
return '%s' % e
except Exception as e:
diff --git a/plexpy/logger.py b/plexpy/logger.py
index ca87544b..e9332a66 100644
--- a/plexpy/logger.py
+++ b/plexpy/logger.py
@@ -88,7 +88,7 @@ class BlacklistFilter(logging.Filter):
Log filter for blacklisted tokens and passwords
"""
def __init__(self):
- pass
+ super(BlacklistFilter, self).__init__()
def filter(self, record):
if not plexpy.CONFIG.LOG_BLACKLIST:
@@ -106,30 +106,29 @@ class BlacklistFilter(logging.Filter):
return True
-class PublicIPFilter(logging.Filter):
+class RegexFilter(logging.Filter):
"""
- Log filter for public IP addresses
+ Base class for regex log filter
"""
def __init__(self):
- pass
+ super(RegexFilter, self).__init__()
+
+ self.regex = re.compile(r'')
def filter(self, record):
if not plexpy.CONFIG.LOG_BLACKLIST:
return True
try:
- # Currently only checking for ipv4 addresses
- ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg)
- for ip in ipv4:
- if is_public_ip(ip):
- record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***')
+ matches = self.regex.findall(record.msg)
+ for match in matches:
+ record.msg = self.replace(record.msg, match)
args = []
for arg in record.args:
- ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', arg) if isinstance(arg, basestring) else []
- for ip in ipv4:
- if is_public_ip(ip):
- arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***')
+ matches = self.regex.findall(arg) if isinstance(arg, basestring) else []
+ for match in matches:
+ arg = self.replace(arg, match)
args.append(arg)
record.args = tuple(args)
except:
@@ -137,31 +136,53 @@ class PublicIPFilter(logging.Filter):
return True
+ def replace(self, text, match):
+ return text
-class PlexTokenFilter(logging.Filter):
+
+class PublicIPFilter(RegexFilter):
+ """
+ Log filter for public IP addresses
+ """
+ def __init__(self):
+ super(PublicIPFilter, self).__init__()
+
+ # Currently only checking for ipv4 addresses
+ self.regex = re.compile(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})')
+
+ def replace(self, text, ip):
+ if is_public_ip(ip):
+ return text.replace(ip, ip.partition('.')[0] + '.***.***.***')
+ return text
+
+
+class EmailFilter(RegexFilter):
+ """
+ Log filter for email addresses
+ """
+ def __init__(self):
+ super(EmailFilter, self).__init__()
+
+ self.regex = re.compile(r'([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@'
+ r'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)',
+ re.IGNORECASE)
+
+ def replace(self, text, email):
+ email_parts = email.partition('@')
+ return text.replace(email, email_parts[0][:2] + 8 * '*' + email_parts[1] + 8 * '*')
+
+
+class PlexTokenFilter(RegexFilter):
"""
Log filter for X-Plex-Token
"""
def __init__(self):
- pass
+ super(PlexTokenFilter, self).__init__()
- def filter(self, record):
- try:
- tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', record.msg)
- for token in tokens:
- record.msg = record.msg.replace(token, 8 * '*' + token[-2:])
+ self.regex = re.compile(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)')
- args = []
- for arg in record.args:
- tokens = re.findall(r'X-Plex-Token(?:=|%3D)([a-zA-Z0-9]+)', arg) if isinstance(arg, basestring) else []
- for token in tokens:
- arg = arg.replace(token, 8 * '*' + token[-2:])
- args.append(arg)
- record.args = tuple(args)
- except:
- pass
-
- return True
+ def replace(self, text, token):
+ return text.replace(token, 8 * '*' + token[-2:])
@contextlib.contextmanager
@@ -302,6 +323,7 @@ def initLogger(console=False, log_dir=False, verbose=False):
for handler in logger.handlers + logger_api.handlers + logger_plex_websocket.handlers:
handler.addFilter(BlacklistFilter())
handler.addFilter(PublicIPFilter())
+ handler.addFilter(EmailFilter())
handler.addFilter(PlexTokenFilter())
# Install exception hooks
diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py
index 51c49080..fad05b38 100644
--- a/plexpy/notification_handler.py
+++ b/plexpy/notification_handler.py
@@ -841,6 +841,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'stream_video_codec_level': notify_params['stream_video_codec_level'],
'stream_video_bitrate': notify_params['stream_video_bitrate'],
'stream_video_bit_depth': notify_params['stream_video_bit_depth'],
+ 'stream_video_chroma_subsampling': notify_params['stream_video_chroma_subsampling'],
+ 'stream_video_color_primaries': notify_params['stream_video_color_primaries'],
+ 'stream_video_color_range': notify_params['stream_video_color_range'],
+ 'stream_video_color_space': notify_params['stream_video_color_space'],
+ 'stream_video_color_trc': notify_params['stream_video_color_trc'],
+ 'stream_video_dynamic_range': notify_params['stream_video_dynamic_range'],
'stream_video_framerate': notify_params['stream_video_framerate'],
'stream_video_full_resolution': notify_params['stream_video_full_resolution'],
'stream_video_ref_frames': notify_params['stream_video_ref_frames'],
@@ -951,6 +957,12 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'video_codec_level': notify_params['video_codec_level'],
'video_bitrate': notify_params['video_bitrate'],
'video_bit_depth': notify_params['video_bit_depth'],
+ 'video_chroma_subsampling': notify_params['video_chroma_subsampling'],
+ 'video_color_primaries': notify_params['video_color_primaries'],
+ 'video_color_range': notify_params['video_color_range'],
+ 'video_color_space': notify_params['video_color_space'],
+ 'video_color_trc': notify_params['video_color_trc'],
+ 'video_dynamic_range': notify_params['video_dynamic_range'],
'video_framerate': notify_params['video_framerate'],
'video_full_resolution': notify_params['video_full_resolution'],
'video_ref_frames': notify_params['video_ref_frames'],
diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py
index 597b0bf0..575e8e80 100644
--- a/plexpy/notifiers.py
+++ b/plexpy/notifiers.py
@@ -1000,8 +1000,8 @@ class ANDROIDAPP(Notifier):
config_option.append({
'label': 'Device',
'description': 'No devices registered. '
- 'Get the Android App and register a device.',
+ ''
+ 'Get the Android App and register a device.',
'input_type': 'help'
})
else:
@@ -1010,8 +1010,8 @@ class ANDROIDAPP(Notifier):
'value': self.config['device_id'],
'name': 'androidapp_device_id',
'description': 'Set your Android app device or '
- 'register a new device with Tautulli.',
+ ''
+ 'register a new device with Tautulli.',
'input_type': 'select',
'select_options': devices
})
@@ -1265,8 +1265,8 @@ class DISCORD(Notifier):
'value': self.config['incl_card'],
'name': 'discord_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.
'
- 'Note: Image Hosting '
+ 'Note: Image Hosting '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -1640,8 +1640,8 @@ class FACEBOOK(Notifier):
'value': self.config['incl_card'],
'name': 'facebook_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.
'
- 'Note: Image Hosting '
+ 'Note: Image Hosting '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -1963,8 +1963,8 @@ class HIPCHAT(Notifier):
'value': self.config['incl_card'],
'name': 'hipchat_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.
'
- 'Note: Image Hosting '
+ 'Note: Image Hosting '
'must be enabled under the notifications settings tab.
'
'Note: This will change the notification type to HTML and emoticons will no longer work.',
'input_type': 'checkbox'
@@ -2185,8 +2185,8 @@ class JOIN(Notifier):
'value': self.config['incl_poster'],
'name': 'join_incl_poster',
'description': 'Include a poster with the notifications.
'
- 'Note: Image Hosting '
+ 'Note: Image Hosting '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -3272,8 +3272,8 @@ class SLACK(Notifier):
'value': self.config['incl_card'],
'name': 'slack_incl_card',
'description': 'Include an info card with a poster and metadata with the notifications.
'
- 'Note: Image Hosting '
+ 'Note: Image Hosting '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
},
@@ -3517,8 +3517,8 @@ class TWITTER(Notifier):
'value': self.config['incl_poster'],
'name': 'twitter_incl_poster',
'description': 'Include a poster with the notifications.
'
- 'Note: Image Hosting '
+ 'Note: Image Hosting '
'must be enabled under the notifications settings tab.',
'input_type': 'checkbox'
}
@@ -3560,7 +3560,12 @@ class WEBHOOK(Notifier):
if webhook_headers:
headers.update(webhook_headers)
- return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, json=webhook_body)
+ if headers['Content-Type'] == 'application/json':
+ data = {'json': webhook_body}
+ else:
+ data = {'data': webhook_body}
+
+ return self.make_request(self.config['hook'], method=self.config['method'], headers=headers, **data)
def _return_config_options(self):
config_option = [{'label': 'Webhook URL',
diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py
index b0a02f02..3d3ce816 100644
--- a/plexpy/pmsconnect.py
+++ b/plexpy/pmsconnect.py
@@ -1272,6 +1272,11 @@ class PmsConnect(object):
'video_codec_level': helpers.get_xml_attr(stream, 'level'),
'video_bitrate': helpers.get_xml_attr(stream, 'bitrate'),
'video_bit_depth': helpers.get_xml_attr(stream, 'bitDepth'),
+ 'video_chroma_subsampling': helpers.get_xml_attr(stream, 'chromaSubsampling'),
+ 'video_color_primaries': helpers.get_xml_attr(stream, 'colorPrimaries'),
+ 'video_color_range': helpers.get_xml_attr(stream, 'colorRange'),
+ 'video_color_space': helpers.get_xml_attr(stream, 'colorSpace'),
+ 'video_color_trc': helpers.get_xml_attr(stream, 'colorTrc'),
'video_frame_rate': helpers.get_xml_attr(stream, 'frameRate'),
'video_ref_frames': helpers.get_xml_attr(stream, 'refFrames'),
'video_height': helpers.get_xml_attr(stream, 'height'),
@@ -1533,7 +1538,7 @@ class PmsConnect(object):
# Get the user details
user_info = session.getElementsByTagName('User')[0]
- user_details = users.Users().get_details(user=helpers.get_xml_attr(user_info, 'title'))
+ user_details = users.Users().get_details(user_id=helpers.get_xml_attr(user_info, 'id'))
# Get the player details
player_info = session.getElementsByTagName('Player')[0]
@@ -1708,6 +1713,11 @@ class PmsConnect(object):
video_id = helpers.get_xml_attr(video_stream_info, 'id')
video_details = {'stream_video_bitrate': helpers.get_xml_attr(video_stream_info, 'bitrate'),
'stream_video_bit_depth': helpers.get_xml_attr(video_stream_info, 'bitDepth'),
+ 'stream_video_chroma_subsampling': helpers.get_xml_attr(video_stream_info, 'chromaSubsampling'),
+ 'stream_video_color_primaries': helpers.get_xml_attr(video_stream_info, 'colorPrimaries'),
+ 'stream_video_color_range': helpers.get_xml_attr(video_stream_info, 'colorRange'),
+ 'stream_video_color_space': helpers.get_xml_attr(video_stream_info, 'colorSpace'),
+ 'stream_video_color_trc': helpers.get_xml_attr(video_stream_info, 'colorTrc'),
'stream_video_codec_level': helpers.get_xml_attr(video_stream_info, 'level'),
'stream_video_ref_frames': helpers.get_xml_attr(video_stream_info, 'refFrames'),
'stream_video_language': helpers.get_xml_attr(video_stream_info, 'language'),
@@ -1718,6 +1728,11 @@ class PmsConnect(object):
else:
video_details = {'stream_video_bitrate': '',
'stream_video_bit_depth': '',
+ 'stream_video_chroma_subsampling': '',
+ 'stream_video_color_primaries': '',
+ 'stream_video_color_range': '',
+ 'stream_video_color_space': '',
+ 'stream_video_color_trc': '',
'stream_video_codec_level': '',
'stream_video_ref_frames': '',
'stream_video_language': '',
@@ -1896,6 +1911,11 @@ class PmsConnect(object):
'video_codec_level': '',
'video_bitrate': '',
'video_bit_depth': '',
+ 'video_chroma_subsampling': '',
+ 'video_color_primaries': '',
+ 'video_color_range': '',
+ 'video_color_space': '',
+ 'video_color_trc': '',
'video_frame_rate': '',
'video_ref_frames': '',
'video_height': '',
@@ -1979,6 +1999,21 @@ class PmsConnect(object):
stream_details['stream_video_resolution'],
stream_details['stream_video_resolution'] + (video_details['stream_video_scan_type'][:1] or 'p'))
+ if helpers.cast_to_int(source_video_details['video_bit_depth']) > 8 \
+ and source_video_details['video_color_space'] == 'bt2020nc':
+ stream_details['video_dynamic_range'] = 'HDR'
+ else:
+ stream_details['video_dynamic_range'] = 'SDR'
+
+ if helpers.cast_to_int(video_details['stream_video_bit_depth']) > 8 \
+ and video_details['stream_video_color_space'] == 'bt2020nc':
+ stream_details['stream_video_dynamic_range'] = 'HDR'
+ else:
+ stream_details['stream_video_dynamic_range'] = 'SDR'
+ else:
+ stream_details['video_dynamic_range'] = ''
+ stream_details['stream_video_dynamic_range'] = ''
+
# Get the quality profile
if media_type in ('movie', 'episode', 'clip') and 'stream_bitrate' in stream_details:
if sync_id:
diff --git a/plexpy/version.py b/plexpy/version.py
index f5dbfde2..9445cf6d 100644
--- a/plexpy/version.py
+++ b/plexpy/version.py
@@ -1,3 +1,3 @@
from __future__ import unicode_literals
PLEXPY_BRANCH = "master"
-PLEXPY_RELEASE_VERSION = "v2.1.39"
+PLEXPY_RELEASE_VERSION = "v2.1.42"
diff --git a/plexpy/webserve.py b/plexpy/webserve.py
index 5543b16c..36722e31 100644
--- a/plexpy/webserve.py
+++ b/plexpy/webserve.py
@@ -2830,7 +2830,11 @@ class WebInterface(object):
"newsletter_password": plexpy.CONFIG.NEWSLETTER_PASSWORD,
"newsletter_inline_styles": checked(plexpy.CONFIG.NEWSLETTER_INLINE_STYLES),
"newsletter_custom_dir": plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR,
- "win_sys_tray": checked(plexpy.CONFIG.WIN_SYS_TRAY)
+ "win_sys_tray": checked(plexpy.CONFIG.WIN_SYS_TRAY),
+ "maxmind_license_key": plexpy.CONFIG.MAXMIND_LICENSE_KEY,
+ "geoip_db": plexpy.CONFIG.GEOIP_DB,
+ "geoip_db_installed": plexpy.CONFIG.GEOIP_DB_INSTALLED,
+ "geoip_db_update_days": plexpy.CONFIG.GEOIP_DB_UPDATE_DAYS
}
return serve_template(templatename="settings.html", title="Settings", config=config, kwargs=kwargs)
@@ -3066,15 +3070,17 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
- def install_geoip_db(self, **kwargs):
+ def install_geoip_db(self, update=False, **kwargs):
""" Downloads and installs the GeoLite2 database """
- result = helpers.install_geoip_db()
+ update = True if update == 'true' else False
+
+ result = helpers.install_geoip_db(update=update)
if result:
- return {'result': 'success', 'message': 'GeoLite2 database installed successful.'}
+ return {'result': 'success', 'message': 'GeoLite2 database installed successful.', 'updated': result}
else:
- return {'result': 'error', 'message': 'GeoLite2 database install failed.'}
+ return {'result': 'error', 'message': 'GeoLite2 database install failed.', 'updated': 0}
@cherrypy.expose
@cherrypy.tools.json_out()