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 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()