Compare commits
535 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49669dc7e0 | ||
|
|
5bdf79606e | ||
|
|
ff3a9e47df | ||
|
|
a18ba24f4a | ||
|
|
baeb744a7c | ||
|
|
d07add383f | ||
|
|
a1f18bc133 | ||
|
|
e00c23bc49 | ||
|
|
0228a356e4 | ||
|
|
b0fa0d534e | ||
|
|
1157fda96c | ||
|
|
bbf774379d | ||
|
|
24c8c4319d | ||
|
|
3b8f9f5892 | ||
|
|
b47ccd06f9 | ||
|
|
8c4292f9ac | ||
|
|
a8fbf8ab1d | ||
|
|
38e9938666 | ||
|
|
93c4a0652e | ||
|
|
2fff6647fd | ||
|
|
36f0f60c49 | ||
|
|
d12b57e1de | ||
|
|
deb16428ed | ||
|
|
a75aba4aee | ||
|
|
bb5aa2be3d | ||
|
|
ef6ef98541 | ||
|
|
89f581f63e | ||
|
|
6081fa329b | ||
|
|
112811f3e2 | ||
|
|
ede07595c3 | ||
|
|
08d65623dd | ||
|
|
7540b5fb34 | ||
|
|
10c54c2d10 | ||
|
|
b8d9c8cc47 | ||
|
|
bac5018b27 | ||
|
|
c65d9898c8 | ||
|
|
d7284c40bd | ||
|
|
1c00f82097 | ||
|
|
c501923f2b | ||
|
|
81b22a8c36 | ||
|
|
beb66396fe | ||
|
|
aaf3de68cf | ||
|
|
c827c9e825 | ||
|
|
5100fdbc96 | ||
|
|
fae3f38a88 | ||
|
|
a8236222fb | ||
|
|
2be4d9b6c9 | ||
|
|
00934b04d2 | ||
|
|
1e28b22c70 | ||
|
|
776061605f | ||
|
|
683e5663e1 | ||
|
|
090011c9a5 | ||
|
|
30b11bce98 | ||
|
|
2a91ec1560 | ||
|
|
908ce1ff8d | ||
|
|
fac47ee68b | ||
|
|
0f8c122ee3 | ||
|
|
893c91a15d | ||
|
|
6d152cf308 | ||
|
|
af2d0446da | ||
|
|
244b03ba3e | ||
|
|
50e0629890 | ||
|
|
c296b38b78 | ||
|
|
f7cdfd3f30 | ||
|
|
44cb2400d0 | ||
|
|
d297597fa6 | ||
|
|
168e74aa23 | ||
|
|
35fa8a749b | ||
|
|
21a1870884 | ||
|
|
1d86187f79 | ||
|
|
558f7873f5 | ||
|
|
a20a52f5f1 | ||
|
|
17bb57d5f5 | ||
|
|
e6aef01508 | ||
|
|
013d957e47 | ||
|
|
99e064e040 | ||
|
|
9e5334ac81 | ||
|
|
fd43cf5dd4 | ||
|
|
9aea663754 | ||
|
|
223e2b2b32 | ||
|
|
ca91adbd53 | ||
|
|
4fffbf8a0c | ||
|
|
c3ea35806e | ||
|
|
1c4df69e61 | ||
|
|
707c30b0af | ||
|
|
4d87666a42 | ||
|
|
b28ac1543a | ||
|
|
1983597cf1 | ||
|
|
69a3b5134f | ||
|
|
53044c75dd | ||
|
|
12056ac2ba | ||
|
|
7c8fb58600 | ||
|
|
da2e7635bd | ||
|
|
f8b75eadc6 | ||
|
|
e9bc767c3b | ||
|
|
1644bbd4b7 | ||
|
|
18b328a387 | ||
|
|
98411f0715 | ||
|
|
fcb3474312 | ||
|
|
6362b51902 | ||
|
|
d79d5d5b39 | ||
|
|
80df8f6191 | ||
|
|
fd9cf7017b | ||
|
|
2eed2d54ca | ||
|
|
0c33e7492a | ||
|
|
dd137e5c36 | ||
|
|
dea9663adf | ||
|
|
767dd20bdc | ||
|
|
c350943041 | ||
|
|
c60340d88b | ||
|
|
276c0e5c7d | ||
|
|
054f116017 | ||
|
|
e9017a8342 | ||
|
|
9cff20ca16 | ||
|
|
6cbfacaeae | ||
|
|
8ebfa20db0 | ||
|
|
5beb4876fb | ||
|
|
c723d33d38 | ||
|
|
f75fca12c8 | ||
|
|
8671707e4d | ||
|
|
a9316ebea1 | ||
|
|
ef86740466 | ||
|
|
57cb755668 | ||
|
|
aa75cf2b73 | ||
|
|
3f8224fec5 | ||
|
|
0b67abb2a2 | ||
|
|
872ef2771e | ||
|
|
3b457304e9 | ||
|
|
974c672a87 | ||
|
|
b9f47df930 | ||
|
|
4c388f60d6 | ||
|
|
d6b31dc542 | ||
|
|
539cd60e92 | ||
|
|
056bcd1488 | ||
|
|
1c58a47073 | ||
|
|
32cf1ada8b | ||
|
|
968132099e | ||
|
|
7b3874dcaa | ||
|
|
0302b2412a | ||
|
|
4ac5329019 | ||
|
|
37bc68573c | ||
|
|
dc8996c4d2 | ||
|
|
1ef9d72534 | ||
|
|
db7225fbad | ||
|
|
2c354ad783 | ||
|
|
b96abc8853 | ||
|
|
c4dc81e8fb | ||
|
|
be753983fe | ||
|
|
1bcb34d7eb | ||
|
|
2243cd1de9 | ||
|
|
1ff58a85dc | ||
|
|
428706d9a7 | ||
|
|
5967636ef9 | ||
|
|
ddc9563de9 | ||
|
|
3d1a1b5e45 | ||
|
|
60b330573e | ||
|
|
76c1558473 | ||
|
|
5fe47d797f | ||
|
|
dd5ba05c88 | ||
|
|
26d825dc09 | ||
|
|
3299ec7c82 | ||
|
|
c25c48c1a6 | ||
|
|
2680162b67 | ||
|
|
7d3d2957c3 | ||
|
|
c12862ffba | ||
|
|
9968f8b6dd | ||
|
|
2ea6ae648c | ||
|
|
75f6ed3fc1 | ||
|
|
39c5156d08 | ||
|
|
2f2069e0ad | ||
|
|
be4e8985b7 | ||
|
|
3e344ce56c | ||
|
|
ca4e6cde3e | ||
|
|
1ad982f5b7 | ||
|
|
280a5ae744 | ||
|
|
b1e27d9bc2 | ||
|
|
92bbf8e994 | ||
|
|
cf49f4d6bf | ||
|
|
721bd606cb | ||
|
|
61865ace64 | ||
|
|
5b88058133 | ||
|
|
2616e14c83 | ||
|
|
f1c4bf6249 | ||
|
|
e884d018ed | ||
|
|
9d925cce03 | ||
|
|
ff8d9f9f4c | ||
|
|
c65d8a1ec6 | ||
|
|
2346033871 | ||
|
|
6103d14a6f | ||
|
|
b0600402dd | ||
|
|
8fca6352cf | ||
|
|
34167495ed | ||
|
|
672377ffb7 | ||
|
|
441572ea12 | ||
|
|
c12c72bbd6 | ||
|
|
dff493c8d9 | ||
|
|
f7bffdc050 | ||
|
|
37df262b24 | ||
|
|
f7bc208fd1 | ||
|
|
40260c33af | ||
|
|
138390ff59 | ||
|
|
a5ef749bce | ||
|
|
667c6fe3d1 | ||
|
|
3320f2a2f9 | ||
|
|
59bb5139f7 | ||
|
|
c1f32674dc | ||
|
|
8b52548016 | ||
|
|
200a85adcf | ||
|
|
960c281659 | ||
|
|
d9d04f4857 | ||
|
|
a053789344 | ||
|
|
0784b0a0db | ||
|
|
b84f214030 | ||
|
|
f15e3a47ea | ||
|
|
82b567f15f | ||
|
|
6e2fa9a3b4 | ||
|
|
16b20976b6 | ||
|
|
22a4478a06 | ||
|
|
7327be1ff8 | ||
|
|
8f4034540c | ||
|
|
45a385f36e | ||
|
|
5aa7192e90 | ||
|
|
c7d51b90c6 | ||
|
|
dcb7486fd6 | ||
|
|
e9585ea15d | ||
|
|
339361d15c | ||
|
|
2b9817319a | ||
|
|
a008b09e09 | ||
|
|
fb8f6ebd8c | ||
|
|
1c3a3876e3 | ||
|
|
dfd0b1bf64 | ||
|
|
e0873ff5ad | ||
|
|
1f2efedf7f | ||
|
|
91446d01a7 | ||
|
|
c45a903f9f | ||
|
|
e6556eaf27 | ||
|
|
28d05ba9fe | ||
|
|
25249e7538 | ||
|
|
d36de5a535 | ||
|
|
f921035ee7 | ||
|
|
f901eaa625 | ||
|
|
9738bdcb55 | ||
|
|
6f6541fdb8 | ||
|
|
08959fd1e6 | ||
|
|
89a03fb263 | ||
|
|
b9e900f540 | ||
|
|
ed6ff9c10c | ||
|
|
5da4fe953e | ||
|
|
302ca85dd3 | ||
|
|
98c0d6619a | ||
|
|
9fdcc93e2e | ||
|
|
74e8d7d329 | ||
|
|
5595ef2e20 | ||
|
|
6ee5eb13f0 | ||
|
|
3afa52b607 | ||
|
|
28227a3c35 | ||
|
|
2d671fdfc4 | ||
|
|
ff67719cf8 | ||
|
|
1761b14fe9 | ||
|
|
8792aa6c70 | ||
|
|
34e548cc15 | ||
|
|
fa25809e7a | ||
|
|
fa71beb03f | ||
|
|
9c955771c0 | ||
|
|
0c6ccc5d52 | ||
|
|
7c6619ebc5 | ||
|
|
22ed8a3a95 | ||
|
|
924ed70458 | ||
|
|
de4d8fb277 | ||
|
|
d61e699dc9 | ||
|
|
ad183ff9fe | ||
|
|
078f4babf5 | ||
|
|
8a989d71ca | ||
|
|
78f959d39a | ||
|
|
20056718db | ||
|
|
6eec4d1ca6 | ||
|
|
7c725ee424 | ||
|
|
4fa70cb234 | ||
|
|
483f5825db | ||
|
|
d23aaf91f7 | ||
|
|
1f3a238ab2 | ||
|
|
060c549259 | ||
|
|
626e7fdf82 | ||
|
|
adc808ac9f | ||
|
|
fc75232519 | ||
|
|
c85ee3aec0 | ||
|
|
881142d4a1 | ||
|
|
c1b5514789 | ||
|
|
7170dbd800 | ||
|
|
d6c21e173d | ||
|
|
179eaf1bbe | ||
|
|
8791babf8e | ||
|
|
048b31c87a | ||
|
|
e386d3ee21 | ||
|
|
285946bf94 | ||
|
|
578ba52215 | ||
|
|
5126c39c26 | ||
|
|
5ec9e41244 | ||
|
|
5eebf6592a | ||
|
|
7994351644 | ||
|
|
e445228b8a | ||
|
|
46e6250329 | ||
|
|
2edfc1e3da | ||
|
|
f2024b0854 | ||
|
|
5f2cf6cb7a | ||
|
|
73664b6a03 | ||
|
|
af131ce16d | ||
|
|
414c4c2ffa | ||
|
|
d2cdc2cea2 | ||
|
|
b860f9a5e8 | ||
|
|
e5535b6167 | ||
|
|
6359c02c80 | ||
|
|
1c589fbefa | ||
|
|
b768ad8a19 | ||
|
|
60878ed12e | ||
|
|
328d744efd | ||
|
|
92a868c3c6 | ||
|
|
7b9210a5fc | ||
|
|
fb872596d6 | ||
|
|
a43efef28a | ||
|
|
e30e6dfe35 | ||
|
|
eaadd5e2d6 | ||
|
|
c608c7c9fc | ||
|
|
451485d706 | ||
|
|
849675185d | ||
|
|
b060a23733 | ||
|
|
bee9091182 | ||
|
|
f8e1ba6798 | ||
|
|
e5ce57fead | ||
|
|
c9e2d1d200 | ||
|
|
88b6eae3bf | ||
|
|
c7d6ee8021 | ||
|
|
7480508af2 | ||
|
|
3fcc44aacf | ||
|
|
fcc4575a86 | ||
|
|
ebd0276eae | ||
|
|
35521e127f | ||
|
|
2c83554631 | ||
|
|
56b717b1a1 | ||
|
|
56f601e2a5 | ||
|
|
3867dd7bdd | ||
|
|
b7dc28c3fb | ||
|
|
e6383d52ad | ||
|
|
0a2ebb8815 | ||
|
|
e44a0fed22 | ||
|
|
2d7585d64b | ||
|
|
df290d995b | ||
|
|
36ee5234b1 | ||
|
|
48e601d5ff | ||
|
|
84aa727387 | ||
|
|
29204cb6ba | ||
|
|
6b75a55ce8 | ||
|
|
527e7be13d | ||
|
|
2d739f64cf | ||
|
|
39ae387e0e | ||
|
|
aaa1f0aa33 | ||
|
|
423360e820 | ||
|
|
52fae6df0e | ||
|
|
58ea4a65e1 | ||
|
|
82025457ba | ||
|
|
9d60ed7174 | ||
|
|
d67f903107 | ||
|
|
389ce97d03 | ||
|
|
98f5c33af7 | ||
|
|
dceab3791f | ||
|
|
d44bd2f35b | ||
|
|
82fc314b35 | ||
|
|
6ff902e653 | ||
|
|
0245668907 | ||
|
|
3436175223 | ||
|
|
8138c27242 | ||
|
|
5705e677d4 | ||
|
|
c2417de895 | ||
|
|
9b03ffbaa1 | ||
|
|
53c98d0acc | ||
|
|
883a183208 | ||
|
|
85a3f15531 | ||
|
|
28d136075c | ||
|
|
a682cd31af | ||
|
|
9fc44f793b | ||
|
|
16b1b2a781 | ||
|
|
6e14c08570 | ||
|
|
b5f1475a90 | ||
|
|
8efb37c4ec | ||
|
|
ab78b59f61 | ||
|
|
e7b1e177d2 | ||
|
|
e41fa7a08e | ||
|
|
0f10c21a66 | ||
|
|
f2b9984cd6 | ||
|
|
99dd133a9a | ||
|
|
7bbcc575b0 | ||
|
|
0fded38bfb | ||
|
|
88a651645a | ||
|
|
60380c6a99 | ||
|
|
298d98cee9 | ||
|
|
b91a4844e0 | ||
|
|
f00de8e548 | ||
|
|
dfbf435c77 | ||
|
|
dd7390d111 | ||
|
|
73125153c8 | ||
|
|
c08d581df3 | ||
|
|
ef5d9d6604 | ||
|
|
84ffcc7948 | ||
|
|
53830a7711 | ||
|
|
a126703f44 | ||
|
|
6720d696eb | ||
|
|
7328fcd0fb | ||
|
|
1e57e952db | ||
|
|
d627e0efdb | ||
|
|
2193d06363 | ||
|
|
3fc573bd90 | ||
|
|
9b9088bd5e | ||
|
|
53fb8b999f | ||
|
|
58efd299cc | ||
|
|
8ae2f718f4 | ||
|
|
ff0ed1abe4 | ||
|
|
41e1fee4b4 | ||
|
|
b308e92c9d | ||
|
|
82808cdc97 | ||
|
|
6d5e7135ae | ||
|
|
4a75e372d9 | ||
|
|
a37ca3f29c | ||
|
|
9016fc41f9 | ||
|
|
f56ba300ac | ||
|
|
0a850fc3af | ||
|
|
5124ebfe16 | ||
|
|
4de252d8e4 | ||
|
|
c5237547cf | ||
|
|
af85d36762 | ||
|
|
41c94741e2 | ||
|
|
8b07b53b43 | ||
|
|
445dd1250f | ||
|
|
4df8014102 | ||
|
|
35320649db | ||
|
|
c62ec71cfc | ||
|
|
b685d17969 | ||
|
|
ba5fd4b9d3 | ||
|
|
79757da2b1 | ||
|
|
77f5224e13 | ||
|
|
31805e39e6 | ||
|
|
ff6d43c398 | ||
|
|
81a8d93336 | ||
|
|
609549f974 | ||
|
|
7ba6d704cd | ||
|
|
0ff1b4d5dd | ||
|
|
7eac521369 | ||
|
|
1d8a16f3c4 | ||
|
|
685f8cabdb | ||
|
|
70141eda22 | ||
|
|
0204478483 | ||
|
|
79f317709b | ||
|
|
0f04a6d25b | ||
|
|
2d19accdd1 | ||
|
|
3d5706002d | ||
|
|
ac3bafcd02 | ||
|
|
ac9a8b85ec | ||
|
|
abec036cb2 | ||
|
|
8bdd40f011 | ||
|
|
b98c17e738 | ||
|
|
a71c6e6592 | ||
|
|
545b7cb581 | ||
|
|
0848e317ee | ||
|
|
e976e6cf5c | ||
|
|
95cd2b4f81 | ||
|
|
3df73dc287 | ||
|
|
3a703eb605 | ||
|
|
e94c00ca48 | ||
|
|
d6a34b3e6b | ||
|
|
3cbf05d54a | ||
|
|
cba8608c23 | ||
|
|
e34865d0dd | ||
|
|
b2a7f639bb | ||
|
|
fcbc921470 | ||
|
|
5168d76e86 | ||
|
|
968d213b97 | ||
|
|
9fc4573c42 | ||
|
|
9adf5cc39a | ||
|
|
2ca04f4a8b | ||
|
|
fd3b2a48f9 | ||
|
|
01b3ae377b | ||
|
|
5a1516286c | ||
|
|
317a9f0b8e | ||
|
|
c98505038a | ||
|
|
1ec1edefdd | ||
|
|
aa351bd965 | ||
|
|
519ff6b203 | ||
|
|
5b2beb2b81 | ||
|
|
13e6a70a30 | ||
|
|
7e99eb7a2a | ||
|
|
6efaabb630 | ||
|
|
2536fdf17b | ||
|
|
7e8a427107 | ||
|
|
bbaf428fd8 | ||
|
|
7dfd063138 | ||
|
|
5c94b21bd1 | ||
|
|
58474d9565 | ||
|
|
6cb1c057cf | ||
|
|
22cc06dec3 | ||
|
|
357797df6b | ||
|
|
4c6f6ca736 | ||
|
|
c0214f1489 | ||
|
|
dd27f9bf72 | ||
|
|
c1c7911d08 | ||
|
|
755e9107fa | ||
|
|
3bb6320fc1 | ||
|
|
b5ad88ae5a | ||
|
|
bbcf3bf7da | ||
|
|
51e1949538 | ||
|
|
6b1a57e650 | ||
|
|
8e57df53fd | ||
|
|
cea4992331 | ||
|
|
c98a8865d6 | ||
|
|
fd3daae491 | ||
|
|
697d107952 | ||
|
|
65e42be278 | ||
|
|
ad79d860db | ||
|
|
5826a823a8 | ||
|
|
cbec1e7768 | ||
|
|
40f72bbe5f | ||
|
|
699f481308 | ||
|
|
411c28a10b | ||
|
|
375bd733f1 | ||
|
|
a96482ee3c | ||
|
|
5fa6489733 | ||
|
|
804a667b19 | ||
|
|
7a7c92191d | ||
|
|
b7baf1a05d | ||
|
|
c4416572cf | ||
|
|
5062c6e67a | ||
|
|
3fc2f43b79 | ||
|
|
a0bd94397c | ||
|
|
ad12a85c6c | ||
|
|
e8e5a0b5ff | ||
|
|
0877a6bf21 | ||
|
|
b0ded77571 |
57
API.md
57
API.md
@@ -4,24 +4,77 @@ The API is still pretty new and needs some serious cleaning up on the backend bu
|
|||||||
## General structure
|
## General structure
|
||||||
The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command`
|
The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command`
|
||||||
|
|
||||||
Data response in JSON formatted.
|
Response example
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"loglevel": "INFO",
|
||||||
|
"msg": "Signal 2 caught, saving and exiting...",
|
||||||
|
"thread": "MainThread",
|
||||||
|
"time": "22-sep-2015 01:42:56 "
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": null,
|
||||||
|
"result": "success"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
General parameters:
|
||||||
|
out_type: 'xml',
|
||||||
|
callback: 'pong',
|
||||||
|
'debug': 1
|
||||||
|
|
||||||
|
|
||||||
## API methods
|
## API methods
|
||||||
|
|
||||||
### getLogs
|
### getLogs
|
||||||
Not working yet
|
Possible params: sort='', search='', order='desc', regex='', start=0, end=0
|
||||||
|
Returns the plexpy log
|
||||||
|
|
||||||
|
### getApikey
|
||||||
|
Possible params: username='', password='' (required if auth is enabled)
|
||||||
|
Returns the apikey
|
||||||
|
|
||||||
|
### getSettings
|
||||||
|
No params
|
||||||
|
Returns the config file
|
||||||
|
|
||||||
### getVersion
|
### getVersion
|
||||||
|
No params
|
||||||
Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
|
Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
|
||||||
|
|
||||||
|
### getHistory
|
||||||
|
possible params: user=None, user_id=None, ,rating_key='', parent_rating_key='', grandparent_rating_key='', start_date=''
|
||||||
|
Returns
|
||||||
|
|
||||||
|
### getMetadata
|
||||||
|
Required params: rating_key
|
||||||
|
Returns metadata about a file
|
||||||
|
|
||||||
|
### getSync
|
||||||
|
Possible params: machine_id=None, user_id=None,
|
||||||
|
Returns
|
||||||
|
|
||||||
|
### getUserips
|
||||||
|
Possible params: user_id=None, user=None
|
||||||
|
|
||||||
|
### getPlayby
|
||||||
|
Possible params: time_range=30, y_axis='plays', playtype='total_plays_per_month'
|
||||||
|
|
||||||
### checkGithub
|
### checkGithub
|
||||||
Updates the version information above and returns getVersion data
|
Updates the version information above and returns getVersion data
|
||||||
|
|
||||||
### shutdown
|
### shutdown
|
||||||
|
No params
|
||||||
Shut down plexpy
|
Shut down plexpy
|
||||||
|
|
||||||
### restart
|
### restart
|
||||||
|
No params
|
||||||
Restart plexpy
|
Restart plexpy
|
||||||
|
|
||||||
### update
|
### update
|
||||||
|
No params
|
||||||
Update plexpy - you may want to check the install type in get version and not allow this if type==exe
|
Update plexpy - you may want to check the install type in get version and not allow this if type==exe
|
||||||
|
|||||||
235
CHANGELOG.md
235
CHANGELOG.md
@@ -1,10 +1,239 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v1.0 (2015-07-11)
|
## v1.2.10 (2015-12-06)
|
||||||
|
|
||||||
* First release
|
* Fix broken count graphs regression.
|
||||||
|
|
||||||
## v1.0.1 (2015-07-13)
|
|
||||||
|
## v1.2.9 (2015-12-06)
|
||||||
|
|
||||||
|
* Fix and improve text sanitization.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.8 (2015-12-06)
|
||||||
|
|
||||||
|
* Fix sanitize player names
|
||||||
|
* Fix recently added notification delay
|
||||||
|
* Fix recently added metadata queries
|
||||||
|
* Fix multiple lines in notification body text
|
||||||
|
* Fix UTF-8 encoding in Prowl notifications subject line
|
||||||
|
* Change to only log IPv4 addresses
|
||||||
|
* Add global toggle for recently added notifcations
|
||||||
|
* Add feature to delete users
|
||||||
|
* Add channel support for Telegram notification agent
|
||||||
|
* Add icon for Apple tvOS
|
||||||
|
* Add icon for Microsoft Edge
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.7 (2015-11-27)
|
||||||
|
|
||||||
|
* Fix IP address option in notifications
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.6 (2015-11-27)
|
||||||
|
|
||||||
|
* Fixes for IP logging in PMS < 0.9.14.x.
|
||||||
|
* Fix issue in plexWatch importer when trying to import item with no ratingKey.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.5 (2015-11-25)
|
||||||
|
|
||||||
|
* Add video_decision and audio_decision to notification options
|
||||||
|
* Fix IP address logging
|
||||||
|
* Fix log spam if notifications disabled
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.4 (2015-11-24)
|
||||||
|
|
||||||
|
* Add filtering by media type in the history table
|
||||||
|
* Add IFTTT notification agent
|
||||||
|
* Add Telegram notification agent
|
||||||
|
* Add notifications for recently added media
|
||||||
|
* Add notifications for server down and remote access down
|
||||||
|
* Add more metadata to notifications options
|
||||||
|
* Add IP address to notification options (for PMS 0.9.14 and above)
|
||||||
|
* Add server uptime to notification options
|
||||||
|
* Add IP address to current activity
|
||||||
|
* Add IPv6 address logging
|
||||||
|
* Add PMS server name to the page title
|
||||||
|
* Fix bug in "Last Watched" statistic
|
||||||
|
* Fix bug in search query
|
||||||
|
* Fix bug on user pages for usernames with single quotes
|
||||||
|
* Fix name for new Plex Media Center
|
||||||
|
* Fix Pushover notifications with unicode characters
|
||||||
|
* Fix bug with showing old usernames in datatables
|
||||||
|
* Fix bug with "Please verify your server" in settings
|
||||||
|
* Change IP lookup provider
|
||||||
|
* Change notifications custom body text to larger text box
|
||||||
|
* Change movie/tv logging and notifications into individual options
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.3 (2015-10-18)
|
||||||
|
|
||||||
|
* Added "remaining time" as notification substitution.
|
||||||
|
* Fix bug on home stats cards.
|
||||||
|
* Fix visual bug on user page.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.2 (2015-10-12)
|
||||||
|
|
||||||
|
* Add server discovery on first run.
|
||||||
|
* Add column to tables for Platform.
|
||||||
|
* Add link to top level breadcrumbs on info pages.
|
||||||
|
* Add ability to change notification sounds for Pushover and Boxcar.
|
||||||
|
* Show watched percentage tooltip on progress column in history tables.
|
||||||
|
* More logging in event an http request fails.
|
||||||
|
* Code cleanups and other fixes.
|
||||||
|
* Fix ordering on sync table.
|
||||||
|
* Fix bug on home stats cards.
|
||||||
|
* Fix bug on activity pane where music details were not shown.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.1 (2015-09-29)
|
||||||
|
|
||||||
|
* Fix for possible issue when paused_counter is null.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.2.0 (2015-09-29)
|
||||||
|
|
||||||
|
* Added option to group consecutive plays in the history tables.
|
||||||
|
* Added option for websocket monitoring (still slightly experimental and disabled by default).
|
||||||
|
* Added global search option (searches your Plex library).
|
||||||
|
* Added option to update any items that may have had their rating keys changed.
|
||||||
|
* Added option to disable consecutive notifications.
|
||||||
|
* Some visual tweaks and fixes.
|
||||||
|
* Fix bug where monitoring wouldn't start up after first run.
|
||||||
|
* Fix bug showing incorrect transcode decisions for music tracks on history tables.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.10 (2015-09-20)
|
||||||
|
|
||||||
|
* Added dedicated settings section for home stats configuration with ability to show/hide selected stats and sections.
|
||||||
|
* Added support for Twitter notifications.
|
||||||
|
* Only show music in graphs if music logging is enabled.
|
||||||
|
* The monitoring ignore interval now excludes paused time.
|
||||||
|
* Fix display bug on activity panel which incorrectly reported transcoding sometimes.
|
||||||
|
* Fix bug with Email notification TLS checkbox when it would be disabled by changing any other settings afterwards.
|
||||||
|
* Fix issue on some Python releases where the webbrowser library isn't included.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.9 (2015-09-14)
|
||||||
|
|
||||||
|
* Another JonnyWong release. I'm going to stop thanking you now ;)
|
||||||
|
* Add music plays to graphs.
|
||||||
|
* Add info pages for music items.
|
||||||
|
* Add music to user recently watched items.
|
||||||
|
* Add photo views to Activity pane (photos are not logged).
|
||||||
|
* Fix token validation message on Settings page.
|
||||||
|
* Fix some "Mystery" platform names.
|
||||||
|
* Fix paused time be counted for graph data.
|
||||||
|
* Other small bug fixes.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.8 (2015-09-09)
|
||||||
|
|
||||||
|
* Add platform images for Windows devices. Thanks @JonnyWong.
|
||||||
|
* Add click-through to PlexWeb preplay page from info page. Thanks @JonnyWong.
|
||||||
|
* Fix broken delete option on info pages. Thanks @JonnyWong.
|
||||||
|
* Fix tagline bug in PlexWatch db import tool.
|
||||||
|
* Fix home stats text overflow bug. Thanks @JonnyWong.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.7 (2015-09-07)
|
||||||
|
|
||||||
|
* Show tagline in info screens for movies. Thanks @JonnyWong.
|
||||||
|
* Add play/pause/buffer icon to activity pane. Thanks @JonnyWong.
|
||||||
|
* Add transcoder info in activity pane info. Thanks @JonnyWong.
|
||||||
|
* Show transcoder progress on activity progress bar. Thanks @JonnyWong.
|
||||||
|
* Fix bug where custom notification strings would be ignored if unicode characters were present.
|
||||||
|
* Fix text overflow issue on home stats cards. Thanks @JonnyWong.
|
||||||
|
* Fix regression with user friendly name change input in edit screen. Thanks @JonnyWong.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.6 (2015-09-06)
|
||||||
|
|
||||||
|
* Home stats cards are now expandable to show multiple items. Configurable in settings. Thanks @JonnyWong.
|
||||||
|
* Completely redesigned media info pages. Thanks @JonnyWong.
|
||||||
|
* Redesigned activity pane to match Plex Web more closely. Thanks @JonnyWong.
|
||||||
|
* New Library stats on home page, shows total item counts per library. Thanks @JonnyWong.
|
||||||
|
* New last watched card in home stats. Shows last watched items. Thanks @JonnyWong.
|
||||||
|
* Improved some layout issues on mobile devices. Thanks @JonnyWong.
|
||||||
|
* Fixed issue where some clip/channel items are reported as episodes and causing exceptions.
|
||||||
|
* Many styling improvements and fixes. Thanks @JonnyWong.
|
||||||
|
* Fixed incorrect sort on home stats platform count by duration. Thanks @JonnyWong.
|
||||||
|
* Fix issue where user refresh would continually be called as "Local" user didn't exist in database.
|
||||||
|
* Fixed styling on graph stream modal. Thanks @JonnyWong.
|
||||||
|
* Fixed some issues with users page editing. Thanks @JonnyWong.
|
||||||
|
* Fix error page when clicking through to an item that no longer exists.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.5 (2015-08-27)
|
||||||
|
|
||||||
|
* Fix git tag being one release behind.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.4 (2015-08-26)
|
||||||
|
|
||||||
|
* User info is now editable from the users table. Thanks @JonnyWong.
|
||||||
|
* Improved delete mode for history pages - able to multi-select now. Thanks @JonnyWong.
|
||||||
|
* Improved image quality on tooltip images.
|
||||||
|
* More styling improvements and fixes on user and info pages. Thanks @JonnyWong.
|
||||||
|
* Added some user submitted systemd init scripts. Thanks @malle-pietje and @artbird309.
|
||||||
|
* Fixed some background operations when saving settings.
|
||||||
|
* Fix max width restricting home stats to 1600px.
|
||||||
|
* Fix stream duration parameter for notifications when paused counter is null.
|
||||||
|
|
||||||
|
|
||||||
|
## v1.1.3 (2015-08-22)
|
||||||
|
|
||||||
|
* Show human readable version info and this cool changelog in Settings -> General.
|
||||||
|
* Add a "delete" mode to the history tables. Toggle it to show a delete button next to each history item.
|
||||||
|
* Two digit season and episode numbers for custom notification messages. Thanks @JonnyWong.
|
||||||
|
* New FreeNAS init script. Thanks @JonnyWong.
|
||||||
|
* Lots of styling improvements! Thanks @JonnyWong.
|
||||||
|
* Graph page remembers last selected options. Thanks @JonnyWong.
|
||||||
|
* New Popular movie homepage stats. Thanks @JonnyWong.
|
||||||
|
* Add option for duration vs play count on home stats. (Settings -> Extra Settings). Thanks @JonnyWong.
|
||||||
|
* Clean up media info pages. Don't show metadata that is missing. Thanks @JonnyWong.
|
||||||
|
* Add clear button to search inputs. Thanks @JonnyWong.
|
||||||
|
* New columns on Users list. Thanks @JonnyWong.
|
||||||
|
* New stream duration option for custom notification messages. Thanks @JonnyWong.
|
||||||
|
* Rad new tooltips on the history pages. Thanks @JonnyWong.
|
||||||
|
* And a lot of small visual changes and fixes. Thanks @JonnyWong.
|
||||||
|
* Fixed IP address modal on user history page.
|
||||||
|
* Fixed "invalid date" showing on monthly plays graph.
|
||||||
|
|
||||||
|
## v1.1.2 (2015-08-16)
|
||||||
|
|
||||||
|
* Fix bug where user refresh would fail under certain circumstances.
|
||||||
|
|
||||||
|
## v1.1.1 (2015-08-15)
|
||||||
|
|
||||||
|
* Added Most watched movie for home stats. Thanks @jroyal.
|
||||||
|
* Added TV show title to recently added text. Thanks @jroyal.
|
||||||
|
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
|
||||||
|
* Fix bug where custom avatar URL would get reset on every user refresh.
|
||||||
|
|
||||||
|
## v1.1.0 (2015-08-15)
|
||||||
|
|
||||||
|
* Add option to disable all history logging per user.
|
||||||
|
* Add option to change user avatar URL. Thanks @jroyal.
|
||||||
|
* Show all users on users table even if they don't yet have history.
|
||||||
|
* Add option to change time frame of statistics on home page (Settings -> Extra Settings). Thanks @jroyal.
|
||||||
|
* Add 7 day period for graphs. Thanks @jroyal.
|
||||||
|
* Add pause, resume and buffer warning notification options.
|
||||||
|
* Add fine tuning settings for buffer warning triggers.
|
||||||
|
* Fix issue with SSL cert verification bypass when method doesn't exist (depends on Python version).
|
||||||
|
* Fix bug on home stats which wouldn't update unless a TV show was first logged.
|
||||||
|
* Fix alignment of bands on daily graphs which highlight weekends.
|
||||||
|
* Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close.
|
||||||
|
* Fix some styling niggles.
|
||||||
|
|
||||||
|
## v1.0.1 (2015-08-13)
|
||||||
|
|
||||||
* Allow SSL certificate check override for certain systems with bad CA stores.
|
* Allow SSL certificate check override for certain systems with bad CA stores.
|
||||||
* Fix typo on graphs page causing date selection to break on Safari.
|
* Fix typo on graphs page causing date selection to break on Safari.
|
||||||
|
|
||||||
|
## v1.0 (2015-08-11)
|
||||||
|
|
||||||
|
* First release
|
||||||
16
PlexPy.py
16
PlexPy.py
@@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
@@ -20,7 +22,7 @@ import sys
|
|||||||
# Ensure lib added to path, before any other imports
|
# Ensure lib added to path, before any other imports
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
|
||||||
|
|
||||||
from plexpy import webstart, logger
|
from plexpy import webstart, logger, web_socket
|
||||||
|
|
||||||
import locale
|
import locale
|
||||||
import time
|
import time
|
||||||
@@ -64,7 +66,7 @@ def main():
|
|||||||
|
|
||||||
# Set up and gather command line arguments
|
# Set up and gather command line arguments
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Python frontend for PlexWatch.')
|
description='A Python based monitoring and tracking tool for Plex Media Server.')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
|
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
|
||||||
@@ -191,6 +193,16 @@ def main():
|
|||||||
# Start the background threads
|
# Start the background threads
|
||||||
plexpy.start()
|
plexpy.start()
|
||||||
|
|
||||||
|
# Open connection for websocket
|
||||||
|
if plexpy.CONFIG.MONITORING_USE_WEBSOCKET:
|
||||||
|
try:
|
||||||
|
web_socket.start_thread()
|
||||||
|
except:
|
||||||
|
logger.warn(u"Websocket :: Unable to open connection.")
|
||||||
|
# Fallback to polling
|
||||||
|
plexpy.POLLING_FAILOVER = True
|
||||||
|
plexpy.initialize_scheduler()
|
||||||
|
|
||||||
# Open webbrowser
|
# Open webbrowser
|
||||||
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch:
|
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch:
|
||||||
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
|
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -1,14 +1,12 @@
|
|||||||
#PlexPy
|
#PlexPy
|
||||||
|
|
||||||
|
[](https://gitter.im/drzoidberg33/plexpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
A python based web application for monitoring, analytics and notifications for Plex Media Server (www.plex.tv).
|
A python based web application for monitoring, analytics and notifications for Plex Media Server (www.plex.tv).
|
||||||
|
|
||||||
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).
|
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).
|
||||||
|
|
||||||
* plexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
|
* PlexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
|
||||||
|
|
||||||
If you'd like to buy me a beer, hit the donate button below.
|
|
||||||
|
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9HZK9BDJLKT6)
|
|
||||||
|
|
||||||
|
|
||||||
###Support
|
###Support
|
||||||
@@ -35,48 +33,63 @@ If you'd like to buy me a beer, hit the donate button below.
|
|||||||
* video type & resolution
|
* video type & resolution
|
||||||
* audio type & channel count.
|
* audio type & channel count.
|
||||||
|
|
||||||
|
* Top statistics on home page with configurable duration and measurement metric:
|
||||||
|
* Most watched TV
|
||||||
|
* Most popular TV
|
||||||
|
* Most watched Movie
|
||||||
|
* Most popular Movie
|
||||||
|
* Most active user
|
||||||
|
* Most active platform
|
||||||
|
|
||||||
* Recently added media and how long ago it was added
|
* Recently added media and how long ago it was added
|
||||||
|
|
||||||
* Global watching history with search/filtering & dynamic column sorting
|
* Global watching history with search/filtering & dynamic column sorting
|
||||||
* date
|
* date
|
||||||
* user
|
* user
|
||||||
* platform
|
* platform
|
||||||
* ip address (if enabled in plexWatch)
|
* ip address
|
||||||
* title
|
* title
|
||||||
* stream information details
|
* stream information details
|
||||||
* start time
|
* start time
|
||||||
* paused duration length
|
* paused duration length
|
||||||
* stop time
|
* stop time
|
||||||
* duration length
|
* duration length
|
||||||
* percentage completed
|
* watched progress
|
||||||
|
* show/hide columns
|
||||||
|
* delete mode - allows deletion of specific history items
|
||||||
|
|
||||||
* Full user list with general information and comparison stats
|
* Full user list with general information and comparison stats
|
||||||
|
|
||||||
* Individual user information
|
* Individual user information
|
||||||
- username and gravatar (if available)
|
* username and gravatar (if available)
|
||||||
- daily, weekly, monthly, all time stats for play count and duration length
|
* daily, weekly, monthly, all time stats for play count and duration length
|
||||||
- individual platform stats for each user
|
* individual platform stats for each user
|
||||||
- public ip address history with last seen date and geo tag location
|
* public ip address history with last seen date and geo tag location
|
||||||
- recently watched content
|
* recently watched content
|
||||||
- watching history
|
* watching history
|
||||||
- synced items
|
* synced items
|
||||||
|
* assign users custom friendly names within PlexPy
|
||||||
|
* assign users custom avatar URL within PlexPy
|
||||||
|
* disable history logging per user
|
||||||
|
* disable notifications per user
|
||||||
|
* option to purge all history per user.
|
||||||
|
|
||||||
* Rich analytics presented using Highcharts graphing
|
* Rich analytics presented using Highcharts graphing
|
||||||
- user-selectable time periods of 30, 90 or 365 days
|
* user-selectable time periods of 30, 90 or 365 days
|
||||||
- daily watch count and duration
|
* daily watch count and duration
|
||||||
- totals by day of week and hours of the day
|
* totals by day of week and hours of the day
|
||||||
- totals by top 10 platform
|
* totals by top 10 platform
|
||||||
- totals by top 10 users
|
* totals by top 10 users
|
||||||
- detailed breakdown by transcode decision
|
* detailed breakdown by transcode decision
|
||||||
- source and stream resolutions
|
* source and stream resolutions
|
||||||
- transcode decision counts by user and platform
|
* transcode decision counts by user and platform
|
||||||
- total monthly counts
|
* total monthly counts
|
||||||
|
|
||||||
* Content information pages
|
* Content information pages
|
||||||
- movies (includes watching history)
|
* movies (includes watching history)
|
||||||
- tv shows (includes watching history)
|
* tv shows (includes watching history)
|
||||||
- tv seasons
|
* tv seasons
|
||||||
- tv episodes (includes watching history)
|
* tv episodes (includes watching history)
|
||||||
|
|
||||||
* Full sync list data on all users syncing items from your library
|
* Full sync list data on all users syncing items from your library
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%
|
<%
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import version
|
from plexpy import version
|
||||||
%>
|
%>
|
||||||
@@ -7,7 +7,7 @@ from plexpy import version
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>PlexPy - ${title}</title>
|
<title>PlexPy - ${title} | ${server_name}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
@@ -30,16 +30,16 @@ from plexpy import version
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||||
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
|
||||||
<div id="updatebar">
|
<div id="updatebar" style="display: none;">
|
||||||
You're running an unknown version of PlexPy. <a href="update">Update</a> or
|
You're running an unknown version of PlexPy. <a href="update">Update</a> or
|
||||||
<a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
<a href="#" id="updateDismiss">Close</a>
|
||||||
</div>
|
</div>
|
||||||
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
|
||||||
<div id="updatebar">
|
<div id="updatebar" style="display: none;">
|
||||||
A <a
|
A <a
|
||||||
href="https://github.com/${plexpy.CONFIG.GIT_USER}/plexpy/compare/${plexpy.CURRENT_VERSION}...${plexpy.LATEST_VERSION}">
|
href="https://github.com/${plexpy.CONFIG.GIT_USER}/plexpy/compare/${plexpy.CURRENT_VERSION}...${plexpy.LATEST_VERSION}" target="_blank">
|
||||||
newer version</a> is available. You're ${plexpy.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or
|
newer version</a> is available. You're ${plexpy.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or
|
||||||
<a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
|
<a href="#" id="updateDismiss">Close</a>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
<nav class="navbar navbar-fixed-top">
|
<nav class="navbar navbar-fixed-top">
|
||||||
@@ -52,26 +52,38 @@ from plexpy import version
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="home">
|
<a class="navbar-brand" href="home">
|
||||||
<img alt="PlexPy" src="interfaces/default/images/logo-plexpy@2x.png" height="40px">
|
<img alt="PlexPy" src="interfaces/default/images/logo-plexpy@2x.png" height="40">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
|
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
|
<li>
|
||||||
|
<form action="search" method="post" class="form" id="search_form">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-textbox">
|
||||||
|
<input type="text" class="form-control" name="query" id="query" aria-label="Search" placeholder="Search..."/>
|
||||||
|
</span>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-dark btn-inactive" type="submit" id="search_button"><i class="fa fa-search"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
% if title=="Home":
|
% if title=="Home":
|
||||||
<li class="active"><a href="home"><i class="fa fa-lg fa-home"></i></a></li>
|
<li class="active"><a href="home"><i class="fa fa-lg fa-home"></i></a></li>
|
||||||
% else:
|
% else:
|
||||||
<li><a href="home"><i class="fa fa-lg fa-home"></i></a></li>
|
<li><a href="home"><i class="fa fa-lg fa-home"></i></a></li>
|
||||||
% endif
|
% endif
|
||||||
% if title=="History":
|
|
||||||
<li class="active"><a href="history">History</a></li>
|
|
||||||
% else:
|
|
||||||
<li><a href="history">History</a></li>
|
|
||||||
% endif
|
|
||||||
% if title=="Users" or title=="User":
|
% if title=="Users" or title=="User":
|
||||||
<li class="active"><a href="users">Users</a></li>
|
<li class="active"><a href="users">Users</a></li>
|
||||||
% else:
|
% else:
|
||||||
<li><a href="users">Users</a></li>
|
<li><a href="users">Users</a></li>
|
||||||
% endif
|
% endif
|
||||||
|
% if title=="History":
|
||||||
|
<li class="active"><a href="history">History</a></li>
|
||||||
|
% else:
|
||||||
|
<li><a href="history">History</a></li>
|
||||||
|
% endif
|
||||||
% if title=="Graphs":
|
% if title=="Graphs":
|
||||||
<li class="active"><a href="graphs">Graphs</a></li>
|
<li class="active"><a href="graphs">Graphs</a></li>
|
||||||
% else:
|
% else:
|
||||||
@@ -99,11 +111,46 @@ from plexpy import version
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
${next.headerIncludes()}
|
${next.headerIncludes()}
|
||||||
${next.body()}
|
<div class="body-container">
|
||||||
|
${next.body()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="interfaces/default/js/jquery-2.1.4.min.js"></script>
|
<script src="interfaces/default/js/jquery-2.1.4.min.js"></script>
|
||||||
<script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script>
|
<script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script>
|
||||||
<script src="interfaces/default/js/script.js"></script>
|
<script src="interfaces/default/js/script.js"></script>
|
||||||
|
<script>
|
||||||
|
$('#updateDismiss').click(function() {
|
||||||
|
$('#updatebar').slideUp('slow');
|
||||||
|
// Set cookie to remember dismiss decision for 1 hour.
|
||||||
|
setCookie('updateDismiss', 'true', 1/24);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!getCookie('updateDismiss')) {
|
||||||
|
$('#updatebar').show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
$('#search_form').submit(function (e) {
|
||||||
|
if ($('#query').hasClass('active') && $('#query').val().trim() != '') {
|
||||||
|
$.ajax({
|
||||||
|
type: 'post',
|
||||||
|
url: 'search',
|
||||||
|
data: { 'query': $('#query').val() }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#search_button').removeClass('btn-inactive');
|
||||||
|
$('#query').clearQueue().val('').animate({ right: '0', width: '250px' }).addClass('active').focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$('#query').on('blur', function (e) {
|
||||||
|
if ($(this).val().trim() == '') {
|
||||||
|
$(this).delay(200).animate({ right: '-250px', width: '0' }, function () {
|
||||||
|
$('#search_button').addClass('btn-inactive');
|
||||||
|
}).removeClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
${next.javascriptIncludes()}
|
${next.javascriptIncludes()}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ ul.ColVis_collection {
|
|||||||
width: 150px;
|
width: 150px;
|
||||||
padding: 8px 8px 4px 8px;
|
padding: 8px 8px 4px 8px;
|
||||||
margin: 10px 0px 0px 0px;
|
margin: 10px 0px 0px 0px;
|
||||||
background-color: rgba( 88, 88, 88, 0.8 );
|
background-color: #444;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 2002;
|
z-index: 2002;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.ColVis_collection li {
|
ul.ColVis_collection li {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
401
data/interfaces/default/css/selectize.bootstrap3.css
Normal file
401
data/interfaces/default/css/selectize.bootstrap3.css
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
/**
|
||||||
|
* selectize.bootstrap3.css (v0.12.1) - Bootstrap 3 Theme
|
||||||
|
* Copyright (c) 2013–2015 Brian Reavis & contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
|
||||||
|
* file except in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||||
|
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
||||||
|
* ANY KIND, either express or implied. See the License for the specific language
|
||||||
|
* governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* @author Brian Reavis <brian@thirdroute.com>
|
||||||
|
*/
|
||||||
|
.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder {
|
||||||
|
visibility: visible !important;
|
||||||
|
background: #f2f2f2 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.06) !important;
|
||||||
|
border: 0 none !important;
|
||||||
|
-webkit-box-shadow: inset 0 0 12px 4px #ffffff;
|
||||||
|
box-shadow: inset 0 0 12px 4px #ffffff;
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after {
|
||||||
|
content: '!';
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-drag_drop .ui-sortable-helper {
|
||||||
|
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.selectize-dropdown-header {
|
||||||
|
position: relative;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-bottom: 1px solid #d0d0d0;
|
||||||
|
background: #f8f8f8;
|
||||||
|
-webkit-border-radius: 4px 4px 0 0;
|
||||||
|
-moz-border-radius: 4px 4px 0 0;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
.selectize-dropdown-header-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
color: #333333;
|
||||||
|
opacity: 0.4;
|
||||||
|
margin-top: -12px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
.selectize-dropdown-header-close:hover {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.selectize-dropdown.plugin-optgroup_columns .optgroup {
|
||||||
|
border-right: 1px solid #f2f2f2;
|
||||||
|
border-top: 0 none;
|
||||||
|
float: left;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child {
|
||||||
|
border-right: 0 none;
|
||||||
|
}
|
||||||
|
.selectize-dropdown.plugin-optgroup_columns .optgroup:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.selectize-dropdown.plugin-optgroup_columns .optgroup-header {
|
||||||
|
border-top: 0 none;
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-remove_button [data-value] {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 24px !important;
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-remove_button [data-value] .remove {
|
||||||
|
z-index: 1;
|
||||||
|
/* fixes ie bug (see #392) */
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 17px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 0 0 0;
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0);
|
||||||
|
-webkit-border-radius: 0 2px 2px 0;
|
||||||
|
-moz-border-radius: 0 2px 2px 0;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-remove_button [data-value] .remove:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-remove_button [data-value].active .remove {
|
||||||
|
border-left-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.selectize-control.plugin-remove_button .disabled [data-value] .remove {
|
||||||
|
border-left-color: rgba(77, 77, 77, 0);
|
||||||
|
}
|
||||||
|
.selectize-control {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.selectize-dropdown,
|
||||||
|
.selectize-input,
|
||||||
|
.selectize-input input {
|
||||||
|
color: #333333;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: 20px;
|
||||||
|
-webkit-font-smoothing: inherit;
|
||||||
|
}
|
||||||
|
.selectize-input,
|
||||||
|
.selectize-control.single .selectize-input.input-active {
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: text;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.selectize-input {
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
padding: 6px 12px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
-webkit-border-radius: 4px;
|
||||||
|
-moz-border-radius: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.selectize-control.multi .selectize-input.has-items {
|
||||||
|
padding: 5px 12px 2px;
|
||||||
|
}
|
||||||
|
.selectize-input.full {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.selectize-input.disabled,
|
||||||
|
.selectize-input.disabled * {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
.selectize-input.focus {
|
||||||
|
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.selectize-input.dropdown-active {
|
||||||
|
-webkit-border-radius: 4px 4px 0 0;
|
||||||
|
-moz-border-radius: 4px 4px 0 0;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
.selectize-input > * {
|
||||||
|
vertical-align: baseline;
|
||||||
|
display: -moz-inline-stack;
|
||||||
|
display: inline-block;
|
||||||
|
zoom: 1;
|
||||||
|
*display: inline;
|
||||||
|
}
|
||||||
|
.selectize-control.multi .selectize-input > div {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 3px 3px 0;
|
||||||
|
padding: 1px 3px;
|
||||||
|
background: #efefef;
|
||||||
|
color: #333333;
|
||||||
|
border: 0 solid rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
.selectize-control.multi .selectize-input > div.active {
|
||||||
|
background: #428bca;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 0 solid rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
.selectize-control.multi .selectize-input.disabled > div,
|
||||||
|
.selectize-control.multi .selectize-input.disabled > div.active {
|
||||||
|
color: #808080;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 0 solid rgba(77, 77, 77, 0);
|
||||||
|
}
|
||||||
|
.selectize-input > input {
|
||||||
|
display: inline-block !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
max-height: none !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
text-indent: 0 !important;
|
||||||
|
border: 0 none !important;
|
||||||
|
background: none !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
-webkit-user-select: auto !important;
|
||||||
|
-webkit-box-shadow: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.selectize-input > input::-ms-clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.selectize-input > input:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
.selectize-input::after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
.selectize-input.dropdown-active::before {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
background: #ffffff;
|
||||||
|
height: 1px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.selectize-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
background: #ffffff;
|
||||||
|
margin: -1px 0 0 0;
|
||||||
|
border-top: 0 none;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
-webkit-border-radius: 0 0 4px 4px;
|
||||||
|
-moz-border-radius: 0 0 4px 4px;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
.selectize-dropdown [data-selectable] {
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.selectize-dropdown [data-selectable] .highlight {
|
||||||
|
background: rgba(255, 237, 40, 0.4);
|
||||||
|
-webkit-border-radius: 1px;
|
||||||
|
-moz-border-radius: 1px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
.selectize-dropdown [data-selectable],
|
||||||
|
.selectize-dropdown .optgroup-header {
|
||||||
|
padding: 3px 12px;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup:first-child .optgroup-header {
|
||||||
|
border-top: 0 none;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup-header {
|
||||||
|
color: #777777;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .active {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .active.create {
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .create {
|
||||||
|
color: rgba(51, 51, 51, 0.5);
|
||||||
|
}
|
||||||
|
.selectize-dropdown-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
.selectize-control.single .selectize-input,
|
||||||
|
.selectize-control.single .selectize-input input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.selectize-control.single .selectize-input.input-active,
|
||||||
|
.selectize-control.single .selectize-input.input-active input {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.selectize-control.single .selectize-input:after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 17px;
|
||||||
|
margin-top: -3px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 5px 0 5px;
|
||||||
|
border-color: #333333 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
.selectize-control.single .selectize-input.dropdown-active:after {
|
||||||
|
margin-top: -4px;
|
||||||
|
border-width: 0 5px 5px 5px;
|
||||||
|
border-color: transparent transparent #333333 transparent;
|
||||||
|
}
|
||||||
|
.selectize-control.rtl.single .selectize-input:after {
|
||||||
|
left: 17px;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
.selectize-control.rtl .selectize-input > input {
|
||||||
|
margin: 0 4px 0 -2px !important;
|
||||||
|
}
|
||||||
|
.selectize-control .selectize-input.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.selectize-dropdown,
|
||||||
|
.selectize-dropdown.form-control {
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
-webkit-border-radius: 4px;
|
||||||
|
-moz-border-radius: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup-header {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup:first-child:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.selectize-dropdown .optgroup:before {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
margin: 9px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
}
|
||||||
|
.selectize-dropdown-content {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
.selectize-dropdown-header {
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
.selectize-input {
|
||||||
|
min-height: 34px;
|
||||||
|
}
|
||||||
|
.selectize-input.dropdown-active {
|
||||||
|
-webkit-border-radius: 4px;
|
||||||
|
-moz-border-radius: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.selectize-input.dropdown-active::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.selectize-input.focus {
|
||||||
|
border-color: #66afe9;
|
||||||
|
outline: 0;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||||
|
}
|
||||||
|
.has-error .selectize-input {
|
||||||
|
border-color: #a94442;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
.has-error .selectize-input:focus {
|
||||||
|
border-color: #843534;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
|
||||||
|
}
|
||||||
|
.selectize-control.multi .selectize-input.has-items {
|
||||||
|
padding-left: 9px;
|
||||||
|
padding-right: 9px;
|
||||||
|
}
|
||||||
|
.selectize-control.multi .selectize-input > div {
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.form-control.selectize-control {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
-webkit-border-radius: 0;
|
||||||
|
-moz-border-radius: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ session_key Returns a unique session id for the active stream
|
|||||||
rating_key Returns the unique identifier for the media item.
|
rating_key Returns the unique identifier for the media item.
|
||||||
media_index Returns the index of the media item.
|
media_index Returns the index of the media item.
|
||||||
parent_media_index Returns the index of the media item's parent.
|
parent_media_index Returns the index of the media item's parent.
|
||||||
type Returns the type of session. Either 'track', 'episode' or 'movie'.
|
media_type Returns the type of session. Either 'track', 'episode' or 'movie'.
|
||||||
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
bif_thumb Returns the location of the item's bif thumbnail. Use with pms_image_proxy.
|
bif_thumb Returns the location of the item's bif thumbnail. Use with pms_image_proxy.
|
||||||
art Returns the location of the item's artwork
|
art Returns the location of the item's artwork
|
||||||
@@ -28,10 +28,16 @@ user Returns the name of the user owning the session.
|
|||||||
user_id Returns the Plex user id if available.
|
user_id Returns the Plex user id if available.
|
||||||
machine_id Returns the machine id of the players being used.
|
machine_id Returns the machine id of the players being used.
|
||||||
friendly_name Returns the friendlly name of the user owning the session.
|
friendly_name Returns the friendlly name of the user owning the session.
|
||||||
|
user_thumb Returns the profile picture of the user owning the session.
|
||||||
state Returns the state of the current session. Either 'playing', 'paused' or 'buffering'.
|
state Returns the state of the current session. Either 'playing', 'paused' or 'buffering'.
|
||||||
title Returns the name of the episode, movie or music track.
|
title Returns the name of the episode, movie or music track.
|
||||||
|
year Returns the year of the episode, movie, or clip.
|
||||||
|
ip_address Returns the ip address of the stream.
|
||||||
player Returns the name of the platform used to play the stream.
|
player Returns the name of the platform used to play the stream.
|
||||||
platform Returns the type of platform used to play the stream.
|
platform Returns the type of platform used to play the stream.
|
||||||
|
throttled Returns true if the transcode session is throttled.
|
||||||
|
transcode_progress Returns the current transcode progress of the item. 0 to 100.
|
||||||
|
transcode_speed Returns the current transcode speed of the item.
|
||||||
audio_decision Returns the audio transcode decision. Either 'transcode', 'copy' or 'direct play'.
|
audio_decision Returns the audio transcode decision. Either 'transcode', 'copy' or 'direct play'.
|
||||||
audio_codec Returns the name of the audio codec.
|
audio_codec Returns the name of the audio codec.
|
||||||
audio_channels Returns the number of audio channels.
|
audio_channels Returns the number of audio channels.
|
||||||
@@ -61,159 +67,218 @@ DOCUMENTATION :: END
|
|||||||
% if data is not None:
|
% if data is not None:
|
||||||
% if data['stream_count'] != '0':
|
% if data['stream_count'] != '0':
|
||||||
% for a in data['sessions']:
|
% for a in data['sessions']:
|
||||||
<div class="instance" id="instance-${a['session_key']}">
|
<div class="dashboard-instance" id="instance-${a['session_key']}">
|
||||||
<div class="poster">
|
% if a['media_type'] == 'movie' or a['media_type'] == 'episode' or a['media_type'] == 'track':
|
||||||
<div class="dashboard-activity-poster-face">
|
<a href="info?item_id=${a['rating_key']}">
|
||||||
% if a['type'] == 'movie' and not a['indexes']:
|
% endif
|
||||||
<img src="pms_image_proxy?img=${a['art']}&width=410&height=230"/>
|
<div class="dashboard-activity-poster">
|
||||||
|
% if a['media_type'] == 'movie' and not a['indexes']:
|
||||||
|
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['art']}&width=500&height=280);"></div>
|
||||||
|
% elif a['media_type'] == 'episode' and not a['indexes']:
|
||||||
|
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['art']}&width=500&height=280);"></div>
|
||||||
% elif a['indexes']:
|
% elif a['indexes']:
|
||||||
<img onload="fadeIn(this)" src="pms_image_proxy?img=${a['bif_thumb']}&width=300&height=169" style="display: none;"/>
|
<div class="dashboard-activity-poster-face bif" style="background-image: url(pms_image_proxy?img=${a['bif_thumb']}&width=500&height=280); display: none;"></div>
|
||||||
% else:
|
% else:
|
||||||
% if a['type'] == 'track':
|
% if a['media_type'] == 'track':
|
||||||
<div class="dashboard-activity-poster-music-bg" style="background-image: url('pms_image_proxy?img=${a['thumb']}&width=300&height=300');"></div>
|
<div class="dashboard-activity-cover-face-bg" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=300);"></div>
|
||||||
% endif
|
<div class="dashboard-activity-cover-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=300);"></div>
|
||||||
% if a['type'] == 'clip':
|
% elif a['media_type'] == 'clip':
|
||||||
<img src="${a['thumb']}"/>
|
% if a['art'][:4] == 'http':
|
||||||
|
<div class="dashboard-activity-poster-face" style="background-image: url(${a['art']});"></div>
|
||||||
|
% elif a['thumb'][:4] == 'http':
|
||||||
|
<div class="dashboard-activity-poster-face" style="background-image: url(${a['thumb']});"></div>
|
||||||
% else:
|
% else:
|
||||||
<img src="pms_image_proxy?img=${a['thumb']}&width=410&height=230&fallback=cover"/>
|
% if a['art']:
|
||||||
|
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['art']}&width=500&height=280);"></div>
|
||||||
|
% else:
|
||||||
|
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=500&height=280);"></div>
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
% elif a['media_type'] == 'photo':
|
||||||
|
<div class="dashboard-activity-poster-face bif" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=500&height=500);"></div>
|
||||||
|
% else:
|
||||||
|
<div class="dashboard-activity-cover-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
<div class="dashboard-activity-button-info">
|
||||||
|
<button type="button" class="btn btn-activity-info btn-lg" data-target="#stream-${a['session_key']}">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="stream-${a['session_key']}" class="dashboard-activity-info-details-overlay">
|
||||||
|
<div class="dashboard-activity-info-details-content">
|
||||||
|
<div id="platform-${a['session_key']}" title="${a['platform']}">
|
||||||
|
<script>
|
||||||
|
$("#platform-${a['session_key']}").html("<div class='dashboard-activity-info-platform-box' style='background-image: url(" + getPlatformImagePath('${a['platform']}') + ");'>");
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-activity-info-platform">
|
||||||
|
<strong>${a['player']}</strong><br />
|
||||||
|
% if a['state'] == 'playing':
|
||||||
|
State <strong>Playing</strong>
|
||||||
|
% elif a['state'] == 'paused':
|
||||||
|
State <strong>Paused</strong>
|
||||||
|
% elif a['state'] == 'buffering':
|
||||||
|
State <strong>Buffering</strong>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
% if a['media_type'] == 'track':
|
||||||
|
% if a['audio_decision'] == 'direct play':
|
||||||
|
Stream <strong>Direct Play</strong>
|
||||||
|
% elif a['audio_decision'] == 'copy':
|
||||||
|
Stream <strong>Direct Stream</strong>
|
||||||
|
% else:
|
||||||
|
Stream <strong>Transcoding
|
||||||
|
(Speed: ${a['transcode_speed']})
|
||||||
|
% if a['throttled'] == '1':
|
||||||
|
(Throttled)
|
||||||
|
% endif
|
||||||
|
</strong>
|
||||||
|
% endif
|
||||||
|
<br />
|
||||||
|
% if a['audio_decision'] == 'direct play':
|
||||||
|
Audio <strong>Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch)</strong>
|
||||||
|
% elif a['audio_decision'] == 'copy':
|
||||||
|
Audio <strong>Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
|
||||||
|
% elif a['audio_decision'] == 'transcode':
|
||||||
|
Audio <strong>Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
|
||||||
|
% endif
|
||||||
|
% elif a['media_type'] == 'episode' or a['media_type'] == 'movie' or a['media_type'] == 'clip':
|
||||||
|
% if a['video_decision'] == 'direct play' and a['audio_decision'] == 'direct play':
|
||||||
|
Stream <strong>Direct Play</strong>
|
||||||
|
% elif a['video_decision'] == 'copy' and a['audio_decision'] == 'copy':
|
||||||
|
Stream <strong>Direct Stream</strong>
|
||||||
|
% else:
|
||||||
|
Stream <strong>Transcoding
|
||||||
|
(Speed: ${a['transcode_speed']})
|
||||||
|
% if a['throttled'] == '1':
|
||||||
|
(Throttled)
|
||||||
|
% endif
|
||||||
|
</strong>
|
||||||
|
% endif
|
||||||
|
<br />
|
||||||
|
% if a['video_decision'] == 'direct play':
|
||||||
|
Video <strong>Direct Play (${a['video_codec']}) (${a['width']}x${a['height']})</strong>
|
||||||
|
% elif a['video_decision'] == 'copy':
|
||||||
|
Video <strong>Direct Stream (${a['transcode_video_codec']}) (${a['width']}x${a['height']})</strong>
|
||||||
|
% elif a['video_decision'] == 'transcode':
|
||||||
|
Video <strong>Transcode (${a['transcode_video_codec']}) (${a['transcode_width']}x${a['transcode_height']})</strong>
|
||||||
|
% endif
|
||||||
|
<br />
|
||||||
|
% if a['audio_decision'] == 'direct play':
|
||||||
|
Audio <strong>Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch)</strong>
|
||||||
|
% elif a['audio_decision'] == 'copy':
|
||||||
|
Audio <strong>Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
|
||||||
|
% elif a['audio_decision'] == 'transcode':
|
||||||
|
Audio <strong>Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
|
||||||
|
% endif
|
||||||
|
% elif a['media_type'] == 'photo':
|
||||||
|
% if a['video_decision'] == 'direct play':
|
||||||
|
Stream <strong>Direct Play</strong>
|
||||||
|
% elif a['video_decision'] == 'copy':
|
||||||
|
Stream <strong>Direct Stream</strong>
|
||||||
|
% else:
|
||||||
|
Stream <strong>
|
||||||
|
Transcoding
|
||||||
|
(Speed: ${a['transcode_speed']})
|
||||||
|
% if a['throttled'] == '1':
|
||||||
|
(Throttled)
|
||||||
|
% endif
|
||||||
|
</strong>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if a['media_type'] != 'photo':
|
||||||
<div class="dashboard-activity-poster-info-bar">
|
<div class="dashboard-activity-poster-info-bar">
|
||||||
<div class="dashboard-activity-poster-info-text">
|
<div class="dashboard-activity-poster-info-ip-address">
|
||||||
% if a['type'] == 'track':
|
% if a['ip_address']:
|
||||||
Track ${a['media_index']}
|
<span>IP: ${a['ip_address']}</span>
|
||||||
% elif a['type'] == 'episode':
|
|
||||||
Season ${a['parent_media_index']}, Episode ${a['media_index']}
|
|
||||||
% else:
|
% else:
|
||||||
${a['title']}
|
<span>IP: N/A</span>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-poster-info-time">
|
<div class="dashboard-activity-poster-info-time">
|
||||||
<span class="progress_time">${a['progress']}</span>/<span class="progress_time">${a['duration']}</span>
|
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class='dashboard-activity-metadata-wrapper'>
|
% if a['media_type'] == 'movie' or a['media_type'] == 'episode' or a['media_type'] == 'track':
|
||||||
<div class='dashboard-activity-instance-overlay'>
|
</a>
|
||||||
<div class='dashboard-activity-metadata-progress-minutes'>
|
% endif
|
||||||
<div class='activity-progress'>
|
<div class="dashboard-activity-progress">
|
||||||
|
<div class="dashboard-activity-progress-bar">
|
||||||
|
<div class="bufferbar" style="width: ${a['transcode_progress']}%">${a['transcode_progress']}%</div>
|
||||||
<div class="bar" style="width: ${a['progress_percent']}%">${a['progress_percent']}%</div>
|
<div class="bar" style="width: ${a['progress_percent']}%">${a['progress_percent']}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-platform" data-toggle="tooltip" data-placement="right" title data-title="${a['player']}" id="platform-${a['session_key']}">
|
<div class="dashboard-activity-metadata-wrapper">
|
||||||
|
<a href="user?user_id=${a['user_id']}">
|
||||||
|
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${a['user_thumb']});"></div>
|
||||||
|
</a>
|
||||||
|
<div class="dashboard-activity-metadata-title">
|
||||||
|
% if a['state'] == 'playing':
|
||||||
|
<i class="fa fa-play"></i>
|
||||||
|
% elif a['state'] == 'paused':
|
||||||
|
<i class="fa fa-pause"></i>
|
||||||
|
% elif a['state'] == 'buffering':
|
||||||
|
<i class="fa fa-spinner"></i>
|
||||||
|
% endif
|
||||||
|
% if a['media_type'] == 'episode':
|
||||||
|
<a href="info?item_id=${a['rating_key']}" title="${a['grandparent_title']} - ${a['title']}">${a['grandparent_title']} - ${a['title']}</a>
|
||||||
|
% elif a['media_type'] == 'movie':
|
||||||
|
<a href="info?item_id=${a['rating_key']}" title="${a['title']}">${a['title']}</a>
|
||||||
|
% elif a['media_type'] == 'clip':
|
||||||
|
<span title="${a['title']}">${a['title']}</span>
|
||||||
|
% elif a['media_type'] == 'track':
|
||||||
|
<a href="info?item_id=${a['rating_key']}" title="${a['grandparent_title']} - ${a['title']}">${a['grandparent_title']} - ${a['title']}</a>
|
||||||
|
% elif a['media_type'] == 'photo':
|
||||||
|
<span title="${a['parent_title']}">${a['parent_title']}</span>
|
||||||
|
% else:
|
||||||
|
<span title="${a['title']}">${a['title']}</span>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-activity-metadata-subtitle">
|
||||||
|
% if a['media_type'] == 'episode':
|
||||||
|
<span title="S${a['parent_media_index']} · E${a['media_index']}">S${a['parent_media_index']} · E${a['media_index']}</span>
|
||||||
|
% elif a['media_type'] == 'movie':
|
||||||
|
<span title="${a['year']}">${a['year']}</span>
|
||||||
|
% elif a['media_type'] == 'track':
|
||||||
|
<a href="info?item_id=${a['parent_rating_key']}" title="${a['parent_title']}">${a['parent_title']}</a>
|
||||||
|
% elif a['media_type'] == 'photo':
|
||||||
|
<span title="${a['title']}">${a['title']}</span>
|
||||||
|
% else:
|
||||||
|
<span title="${a['year']}">${a['year']}</span>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-user">
|
<div class="dashboard-activity-metadata-user">
|
||||||
% if a['user_id']:
|
% if a['user_id']:
|
||||||
<a href="user?user_id=${a['user_id']}">${a['friendly_name']}</a> is ${a['state']}
|
<a href="user?user_id=${a['user_id']}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||||
% else:
|
% else:
|
||||||
<a href="user?user=${a['user']}">${a['friendly_name']}</a> is ${a['state']}
|
<a href="user?user=${a['user']}" title="${a['friendly_name']}">${a['friendly_name']}</a>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-activity-metadata-title">
|
|
||||||
% if a['type'] == 'episode':
|
|
||||||
<a href="info?item_id=${a['rating_key']}">${a['grandparent_title']} - ${a['title']}</a>
|
|
||||||
% elif a['type'] == 'movie':
|
|
||||||
<a href="info?item_id=${a['rating_key']}">${a['title']}</a>
|
|
||||||
% elif a['type'] == 'clip':
|
|
||||||
${a['title']}
|
|
||||||
% elif a['type'] == 'track':
|
|
||||||
${a['grandparent_title']} - ${a['title']}
|
|
||||||
% else:
|
|
||||||
${a['grandparent_title']} - ${a['title']}
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="stream-${a['session_key']}" class="collapse out">
|
|
||||||
<div class='dashboard-activity-info-details-overlay'>
|
|
||||||
<div class='dashboard-activity-info-details-content'>
|
|
||||||
% if a['type'] == 'track':
|
|
||||||
Artist: <strong>${a['grandparent_title']}</strong>
|
|
||||||
<br>
|
|
||||||
Album: <strong>${a['parent_title']}</strong>
|
|
||||||
<br>
|
|
||||||
% endif
|
|
||||||
% if a['state'] == 'playing':
|
|
||||||
State: <strong>Playing</strong>
|
|
||||||
% elif a['state'] == 'paused':
|
|
||||||
State: <strong>Paused</strong>
|
|
||||||
% elif a['state'] == 'buffering':
|
|
||||||
State: <strong>Buffering</strong>
|
|
||||||
% endif
|
|
||||||
<br>
|
|
||||||
% if a['type'] == 'track':
|
|
||||||
% if a['audio_decision'] == 'direct play':
|
|
||||||
Stream: <strong>Direct Play</strong>
|
|
||||||
% else:
|
|
||||||
Stream: <strong>Transcoding</strong>
|
|
||||||
% endif
|
|
||||||
<br/>
|
|
||||||
% if a['audio_decision'] != 'direct play':
|
|
||||||
Audio: <strong>${a['transcode_audio_codec']} (${a['transcode_audio_channels']}ch)</strong>
|
|
||||||
% elif a['audio_decision'] == 'direct play':
|
|
||||||
Audio: <strong>${a['audio_codec']} (${a['audio_channels']}ch)</strong>
|
|
||||||
% endif
|
|
||||||
% elif a['type'] == 'episode' or a['type'] == 'movie' or a['type'] == 'clip':
|
|
||||||
% if a['video_decision'] == 'direct play':
|
|
||||||
Stream: <strong>Direct Play</strong>
|
|
||||||
% else:
|
|
||||||
Stream: <strong>Transcoding</strong>
|
|
||||||
% endif
|
|
||||||
<br/>
|
|
||||||
% if a['video_decision'] != 'direct play':
|
|
||||||
Video: <strong>${a['video_decision']} (${a['transcode_video_codec']})
|
|
||||||
(${a['transcode_width']}x${a['transcode_height']})</strong>
|
|
||||||
% elif a['audio_decision'] == 'direct play':
|
|
||||||
Video: <strong>${a['video_decision']} (${a['video_codec']})
|
|
||||||
(${a['width']}x${a['height']})</strong>
|
|
||||||
% endif
|
|
||||||
<br/>
|
|
||||||
% if a['audio_decision'] != 'direct play':
|
|
||||||
Audio: <strong>${a['audio_decision']} ${a['transcode_audio_codec']} (${a['transcode_audio_channels']}ch)</strong>
|
|
||||||
% elif a['audio_decision'] == 'direct play':
|
|
||||||
Audio: <strong>${a['audio_codec']} (${a['audio_channels']}ch)</strong>
|
|
||||||
% endif
|
|
||||||
% endif
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dashboard-activity-button-info">
|
|
||||||
<button type="button" class="btn btn-activity-info btn-sm" data-toggle="collapse" data-target="#stream-${a['session_key']}">
|
|
||||||
<i class='fa fa-info-circle'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
</div>
|
||||||
$("#platform-${a['session_key']}").html("<img src='" + getPlatformImagePath('${a['platform']}') + "'>");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
% endfor
|
% endfor
|
||||||
<script>
|
<script>
|
||||||
// When using bif indexes make the image transition a little smoother.
|
// When using bif indexes make the image transition a little smoother.
|
||||||
function fadeIn(obj) {
|
$('.bif').each(function() {
|
||||||
$(obj).fadeIn(450);
|
$(this).hide().fadeIn(1000);
|
||||||
}
|
});
|
||||||
|
|
||||||
// Convert timestamps to readable times
|
// Convert timestamps to readable times
|
||||||
$('.progress_time').each(function(index) {
|
$('.progress_time').each(function(index) {
|
||||||
$(this).html(millisecondsToMinutes($(this).text(), false));
|
$(this).html(millisecondsToMinutes($(this).text(), false));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hide the info bar on page load
|
// Show/Hide activity info
|
||||||
$('.dashboard-activity-poster-info-bar').hide();
|
$('.btn-activity-info').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
// When mouse over the activity pane, show an info bar with extra info.
|
$($(this).attr('data-target')).toggle();
|
||||||
$('.dashboard-activity-poster-face').mouseenter(function() {
|
|
||||||
$('.dashboard-activity-poster-info-bar', this).slideDown('fast');
|
|
||||||
});
|
|
||||||
$('.dashboard-activity-poster-face').mouseleave(function() {
|
|
||||||
$('.dashboard-activity-poster-info-bar', this).slideUp('fast');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tooltips
|
|
||||||
$('.dashboard-activity-metadata-platform').each(function() {
|
|
||||||
$(this).tooltip();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
% else:
|
% else:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ user Return the real Plex username
|
|||||||
user_id Return the Plex user_id
|
user_id Return the Plex user_id
|
||||||
friendly_name Returns the friendly edited Plex username
|
friendly_name Returns the friendly edited Plex username
|
||||||
do_notify Returns bool value for whether the user should trigger notifications
|
do_notify Returns bool value for whether the user should trigger notifications
|
||||||
|
keep_history Returns bool value for whether the user's activity should be logged
|
||||||
|
|
||||||
DOCUMENTATION :: END
|
DOCUMENTATION :: END
|
||||||
</%doc>
|
</%doc>
|
||||||
@@ -30,18 +31,39 @@ DOCUMENTATION :: END
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="friendly_name">Friendly Name</label>
|
<label for="friendly_name">Friendly Name</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-6">
|
<div class="col-md-6">
|
||||||
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${data['friendly_name']}" size="30">
|
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${data['friendly_name']}" size="30">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">Replace all occurances of the username with this name.</p>
|
<p class="help-block">Replace all occurances of the username with this name.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="profile_url">Profile Picture URL</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="text" class="form-control" id="profile_url" name="profile_url" value="${data['thumb']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">Change the users profile picture in PlexPy. To reset to default, leave this field empty and save then perform a user refresh.</p>
|
||||||
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${data['do_notify']}> Enable notifications
|
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${data['do_notify']}> Enable notifications
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">Uncheck this if you do not want to receive notifications for this user's activity.</p>
|
<p class="help-block">Uncheck this if you do not want to receive notifications for this user's activity.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${data['keep_history']}> Keep history
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p>
|
||||||
|
</div>
|
||||||
|
% if data['user_id']:
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-danger" id="delete-all-history">Purge</button>
|
||||||
|
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -52,32 +74,56 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="confirm-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Confirm Purge</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<p>Are you REALLY sure you want to purge all history for this user?</p>
|
||||||
|
<p>This is permanent and cannot be undone!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-purge">Purge</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Set new friendly name
|
// Set new friendly name
|
||||||
$("#save_user_name").click(function() {
|
$("#save_user_name").click(function() {
|
||||||
var friendly_name = $("#friendly_name").val();
|
var friendly_name = $("#friendly_name").val();
|
||||||
|
var thumb = $("#profile_url").val();
|
||||||
var do_notify = 0;
|
var do_notify = 0;
|
||||||
|
var keep_history = 0;
|
||||||
if ($("#do_notify").is(":checked")) {
|
if ($("#do_notify").is(":checked")) {
|
||||||
do_notify = 1;
|
do_notify = 1;
|
||||||
}
|
}
|
||||||
|
if ($("#keep_history").is(":checked")) {
|
||||||
|
keep_history = 1;
|
||||||
|
}
|
||||||
|
|
||||||
% if data['user_id']:
|
% if data['user_id']:
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'edit_user',
|
url: 'edit_user',
|
||||||
data: {user_id: '${data['user_id']}', friendly_name: friendly_name, do_notify: do_notify},
|
data: {user_id: '${data['user_id']}', friendly_name: friendly_name, do_notify: do_notify, keep_history: keep_history, thumb: thumb},
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
$("#edit-user-status-message").html(data);
|
$("#edit-user-status-message").html(data);
|
||||||
if ($.trim(friendly_name) !== '') {
|
if ($.trim(friendly_name) !== '') {
|
||||||
$(".set-username").html(friendly_name);
|
$('.set-username').html(document.createTextNode(friendly_name));
|
||||||
}
|
}
|
||||||
|
$("#user-profile-thumb").attr('src', thumb);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
% else:
|
% else:
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'edit_user',
|
url: 'edit_user',
|
||||||
data: {user: '${data['user']}', friendly_name: friendly_name, do_notify: do_notify},
|
data: {user: '${data['user']}', friendly_name: friendly_name, do_notify: do_notify, keep_history: keep_history, thumb: thumb},
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
@@ -85,10 +131,53 @@ DOCUMENTATION :: END
|
|||||||
if ($.trim(friendly_name) !== '') {
|
if ($.trim(friendly_name) !== '') {
|
||||||
$(".set-username").html(friendly_name);
|
$(".set-username").html(friendly_name);
|
||||||
}
|
}
|
||||||
|
$("#user-profile-thumb").attr('src', thumb);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
% endif
|
% endif
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#delete-all-history").on('click', function() {
|
||||||
|
$('#confirm-modal').modal();
|
||||||
|
$('#confirm-modal').one('click', '#confirm-purge', function () {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_all_user_history',
|
||||||
|
data: {user_id: '${data['user_id']}'},
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
success: function(data) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Move #confirm-modal to parent container
|
||||||
|
if(!($('#edit-user-modal').next().is('#confirm-modal'))) {
|
||||||
|
$('#confirm-modal').appendTo($('#edit-user-modal').parent()); }
|
||||||
|
$('#edit-user-modal > #confirm-modal').remove();
|
||||||
|
|
||||||
|
$('#edit-user-modal').css('z-index', '1050');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1049');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
|
||||||
|
|
||||||
|
$('#confirm-modal').on('show.bs.modal', function () {
|
||||||
|
// Fix position to match parent modal
|
||||||
|
var currentPadding = parseInt($('body').css('padding-right'));
|
||||||
|
$(this).children('.modal-dialog').css('left', -currentPadding/2);
|
||||||
|
$('#edit-user-modal').css('overflow-y', 'hidden');
|
||||||
|
});
|
||||||
|
$('#confirm-modal').on('shown.bs.modal', function () {
|
||||||
|
$(this).css('z-index', '1060');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1059');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
|
||||||
|
});
|
||||||
|
$('#confirm-modal').on('hidden.bs.modal', function() {
|
||||||
|
$('body').addClass('modal-open');
|
||||||
|
$('#edit-user-modal').css('overflow-y', 'auto');
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group" data-toggle="buttons" id="days-selection">
|
<div class="btn-group" data-toggle="buttons" id="days-selection">
|
||||||
|
<label class="btn btn-dark">
|
||||||
|
<input type="radio" name="date-options" id="graph-7" value="7" autocomplete="off"> 7 days
|
||||||
|
</label>
|
||||||
<label class="btn btn-dark active">
|
<label class="btn btn-dark active">
|
||||||
<input type="radio" name="date-options" id="graph-30" value="30" autocomplete="off" checked> 30 days
|
<input type="radio" name="date-options" id="graph-30" value="30" autocomplete="off" checked> 30 days
|
||||||
</label>
|
</label>
|
||||||
@@ -45,7 +48,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The total play count or duration of movies and tv played per day. Click a graph point to open up a list of items played for that specific date.
|
The total play count or duration of tv, movies, and music played per day. Click a graph point to open up a list of items played for that specific date.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_day">
|
<div class="watch-chart" id="chart_div_plays_by_day">
|
||||||
@@ -59,7 +62,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-calendar"></i> <span class="yaxis-text">Play count</span> by day of week <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-calendar"></i> <span class="yaxis-text">Play count</span> by day of week <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv played per day of the week.
|
The combined total of tv, movies, and music played per day of the week.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_dayofweek" style="float: left;">
|
<div class="watch-chart" id="chart_div_plays_by_dayofweek" style="float: left;">
|
||||||
@@ -72,7 +75,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-clock-o"></i> <span class="yaxis-text">Play count</span> by hour of day <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-clock-o"></i> <span class="yaxis-text">Play count</span> by hour of day <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv played per hour of the day.
|
The combined total of tv, movies, and music played per hour of the day.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_hourofday">
|
<div class="watch-chart" id="chart_div_plays_by_hourofday">
|
||||||
@@ -87,7 +90,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by top 10 platforms <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by top 10 platforms <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv played by top 10 most active platforms.
|
The combined total of tv, movies, and music played by top 10 most active platforms.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_platform" style="float: left;">
|
<div class="watch-chart" id="chart_div_plays_by_platform" style="float: left;">
|
||||||
@@ -100,7 +103,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by top 10 users <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by top 10 users <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv played by top 10 most active users.
|
The combined total of tv, movies, and music played by top 10 most active users.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_user">
|
<div class="watch-chart" id="chart_div_plays_by_user">
|
||||||
@@ -118,7 +121,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h4><i class="fa fa-video-camera"></i> Daily Stream type breakdown <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-video-camera"></i> Daily Stream type breakdown <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The total play count or duration of movies and tv by the transcode decision. Click a graph point to open up a list of items played for that specific date.
|
The total play count or duration of tv, movies, and music by the transcode decision. Click a graph point to open up a list of items played for that specific date.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_stream_type">
|
<div class="watch-chart" id="chart_div_plays_by_stream_type">
|
||||||
@@ -132,7 +135,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv by their original resolution (pre-transcoding).
|
The combined total of tv and movies by their original resolution (pre-transcoding).
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_source_resolution" style="float: left;">
|
<div class="watch-chart" id="chart_div_plays_by_source_resolution" style="float: left;">
|
||||||
@@ -145,7 +148,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv by their streamed resolution (post-transcoding).
|
The combined total of tv and movies by their streamed resolution (post-transcoding).
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_stream_resolution">
|
<div class="watch-chart" id="chart_div_plays_by_stream_resolution">
|
||||||
@@ -160,7 +163,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by platform and stream type <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by platform and stream type <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv by platform and stream type.
|
The combined total of tv, movies, and music by platform and stream type.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_platform_by_stream_type" style="float: left;">
|
<div class="watch-chart" id="chart_div_plays_by_platform_by_stream_type" style="float: left;">
|
||||||
@@ -173,7 +176,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by user and stream type <small>Last <span class="days">30</span> days</small></h4>
|
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by user and stream type <small>Last <span class="days">30</span> days</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv by user and stream type.
|
The combined total of tv, movies, and music by user and stream type.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_user_by_stream_type" style="float: left;">
|
<div class="watch-chart" id="chart_div_plays_by_user_by_stream_type" style="float: left;">
|
||||||
@@ -191,7 +194,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h4><i class="fa fa-calendar"></i> Plays by Month <small>Last 12 months</small></h4>
|
<h4><i class="fa fa-calendar"></i> Plays by Month <small>Last 12 months</small></h4>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
The combined total of movies and tv by month.
|
The combined total of tv, movies, and music by month.
|
||||||
</p>
|
</p>
|
||||||
<div class="graphs-instance">
|
<div class="graphs-instance">
|
||||||
<div class="watch-chart" id="chart_div_plays_by_month">
|
<div class="watch-chart" id="chart_div_plays_by_month">
|
||||||
@@ -260,10 +263,39 @@
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
|
||||||
var current_range = 30;
|
// Save graph state to cookies
|
||||||
|
$('input[name=yaxis-options]').change(function() {
|
||||||
|
setCookie('graphType', $(this).val(), 365, '/');
|
||||||
|
});
|
||||||
|
$('input[name=date-options]').change(function() {
|
||||||
|
setCookie('graphDate', $(this).val(), 365, '/');
|
||||||
|
});
|
||||||
|
$('a[data-toggle=tab]').click(function() {
|
||||||
|
setCookie('graphTab', $(this).attr('href'), 365, '/');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial values for graph if no saved state
|
||||||
var yaxis = 'plays';
|
var yaxis = 'plays';
|
||||||
|
var current_range = 30;
|
||||||
var current_tab = '#tabs-1';
|
var current_tab = '#tabs-1';
|
||||||
|
|
||||||
|
// Read saved graph state from cookies and set initial values
|
||||||
|
if(getCookie('graphType')) {
|
||||||
|
var yaxis = getCookie('graphType');
|
||||||
|
$('input[name=yaxis-options][value=' + yaxis + ']').prop('checked', true).trigger('click');
|
||||||
|
}
|
||||||
|
if(getCookie('graphDate')) {
|
||||||
|
var current_range = getCookie('graphDate');
|
||||||
|
$('input[name=date-options][value=' + current_range + ']').prop('checked', true).trigger('click');
|
||||||
|
$('.days').html(current_range);
|
||||||
|
}
|
||||||
|
if(getCookie('graphTab')) {
|
||||||
|
var current_tab = getCookie('graphTab');
|
||||||
|
$('a[data-toggle=tab][href=' + current_tab + ']').trigger('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
var music_visible = (${config['music_logging_enable']} == 1 ? true : false);
|
||||||
|
|
||||||
function loadGraphsTab1(time_range, yaxis) {
|
function loadGraphsTab1(time_range, yaxis) {
|
||||||
setGraphFormat(yaxis);
|
setGraphFormat(yaxis);
|
||||||
|
|
||||||
@@ -280,8 +312,8 @@
|
|||||||
if ((moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sat') ||
|
if ((moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sat') ||
|
||||||
(moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sun')) {
|
(moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sun')) {
|
||||||
hc_plays_by_day_options.xAxis.plotBands.push({
|
hc_plays_by_day_options.xAxis.plotBands.push({
|
||||||
from: i,
|
from: i-0.5,
|
||||||
to: i+1,
|
to: i+0.5,
|
||||||
color: 'rgba(80,80,80,0.3)'
|
color: 'rgba(80,80,80,0.3)'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -289,6 +321,7 @@
|
|||||||
hc_plays_by_day_options.yAxis.min = 0;
|
hc_plays_by_day_options.yAxis.min = 0;
|
||||||
hc_plays_by_day_options.xAxis.categories = dateArray;
|
hc_plays_by_day_options.xAxis.categories = dateArray;
|
||||||
hc_plays_by_day_options.series = data.series;
|
hc_plays_by_day_options.series = data.series;
|
||||||
|
hc_plays_by_day_options.series[2].visible = music_visible;
|
||||||
var hc_plays_by_day = new Highcharts.Chart(hc_plays_by_day_options);
|
var hc_plays_by_day = new Highcharts.Chart(hc_plays_by_day_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -301,6 +334,7 @@
|
|||||||
success: function(data) {
|
success: function(data) {
|
||||||
hc_plays_by_dayofweek_options.xAxis.categories = data.categories;
|
hc_plays_by_dayofweek_options.xAxis.categories = data.categories;
|
||||||
hc_plays_by_dayofweek_options.series = data.series;
|
hc_plays_by_dayofweek_options.series = data.series;
|
||||||
|
hc_plays_by_dayofweek_options.series[2].visible = music_visible;
|
||||||
var hc_plays_by_dayofweek = new Highcharts.Chart(hc_plays_by_dayofweek_options);
|
var hc_plays_by_dayofweek = new Highcharts.Chart(hc_plays_by_dayofweek_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -313,6 +347,7 @@
|
|||||||
success: function(data) {
|
success: function(data) {
|
||||||
hc_plays_by_hourofday_options.xAxis.categories = data.categories;
|
hc_plays_by_hourofday_options.xAxis.categories = data.categories;
|
||||||
hc_plays_by_hourofday_options.series = data.series;
|
hc_plays_by_hourofday_options.series = data.series;
|
||||||
|
hc_plays_by_hourofday_options.series[2].visible = music_visible;
|
||||||
var hc_plays_by_hourofday = new Highcharts.Chart(hc_plays_by_hourofday_options);
|
var hc_plays_by_hourofday = new Highcharts.Chart(hc_plays_by_hourofday_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -325,6 +360,7 @@
|
|||||||
success: function(data) {
|
success: function(data) {
|
||||||
hc_plays_by_platform_options.xAxis.categories = data.categories;
|
hc_plays_by_platform_options.xAxis.categories = data.categories;
|
||||||
hc_plays_by_platform_options.series = data.series;
|
hc_plays_by_platform_options.series = data.series;
|
||||||
|
hc_plays_by_platform_options.series[2].visible = music_visible;
|
||||||
var hc_plays_by_platform = new Highcharts.Chart(hc_plays_by_platform_options);
|
var hc_plays_by_platform = new Highcharts.Chart(hc_plays_by_platform_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -337,6 +373,7 @@
|
|||||||
success: function(data) {
|
success: function(data) {
|
||||||
hc_plays_by_user_options.xAxis.categories = data.categories;
|
hc_plays_by_user_options.xAxis.categories = data.categories;
|
||||||
hc_plays_by_user_options.series = data.series;
|
hc_plays_by_user_options.series = data.series;
|
||||||
|
hc_plays_by_user_options.series[2].visible = music_visible;
|
||||||
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
|
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -358,8 +395,8 @@
|
|||||||
if ((moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sat') ||
|
if ((moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sat') ||
|
||||||
(moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sun')) {
|
(moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sun')) {
|
||||||
hc_plays_by_stream_type_options.xAxis.plotBands.push({
|
hc_plays_by_stream_type_options.xAxis.plotBands.push({
|
||||||
from: i,
|
from: i-0.5,
|
||||||
to: i+1,
|
to: i+0.5,
|
||||||
color: 'rgba(80,80,80,0.3)'
|
color: 'rgba(80,80,80,0.3)'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -429,13 +466,10 @@
|
|||||||
data: { y_axis: yaxis },
|
data: { y_axis: yaxis },
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
var dateArray = [];
|
|
||||||
for (var i = 0; i < data.categories.length; i++) {
|
|
||||||
dateArray.push(moment(data.categories[i], 'YYYY-MM').format('MMM YYYY'));
|
|
||||||
}
|
|
||||||
hc_plays_by_month_options.yAxis.min = 0;
|
hc_plays_by_month_options.yAxis.min = 0;
|
||||||
hc_plays_by_month_options.xAxis.categories = dateArray;
|
hc_plays_by_month_options.xAxis.categories = data.categories;
|
||||||
hc_plays_by_month_options.series = data.series;
|
hc_plays_by_month_options.series = data.series;
|
||||||
|
hc_plays_by_month_options.series[2].visible = music_visible;
|
||||||
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
|
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,23 +13,30 @@
|
|||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-history"></i> History</span>
|
<span><i class="fa fa-history"></i> History</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="colvis-button-bar hidden-xs">
|
<div class="button-bar">
|
||||||
|
<div class="colvis-button-bar hidden-xs"></div>
|
||||||
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||||
|
<i class="fa fa-trash-o"></i> Delete mode
|
||||||
|
</button>
|
||||||
|
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select rows to delete. Data is deleted upon exiting delete mode.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='table-card-back'>
|
<div class='table-card-back'>
|
||||||
<table class="display" id="history_table" width="100%">
|
<table class="display" id="history_table" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th align='left' id="delete_row">Delete</th>
|
||||||
<th align='left' id="time">Time</th>
|
<th align='left' id="time">Time</th>
|
||||||
<th align='left' id="friendly_name">User</th>
|
<th align='left' id="friendly_name">User</th>
|
||||||
<th align='left' id="platform">Platform</th>
|
|
||||||
<th align='left' id="ip_address">IP Address</th>
|
<th align='left' id="ip_address">IP Address</th>
|
||||||
|
<th align='left' id="platform">Platform</th>
|
||||||
|
<th align='left' id="device">Player</th>
|
||||||
<th align='left' id="title">Title</th>
|
<th align='left' id="title">Title</th>
|
||||||
<th align='left' id="started">Started</th>
|
<th align='left' id="started">Started</th>
|
||||||
<th align='left' id="paused_counter">Paused</th>
|
<th align='left' id="paused_counter">Paused</th>
|
||||||
<th align='left' id="stopped">Stopped</th>
|
<th align='left' id="stopped">Stopped</th>
|
||||||
<th align='left' id="duration">Duration</th>
|
<th align='left' id="duration">Duration</th>
|
||||||
<th align='left' id="percent_complete">Watched</th>
|
<th align='left' id="percent_complete"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -39,6 +46,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Confirm Delete</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
|
||||||
|
<p>This is permanent and cannot be undone!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
@@ -51,18 +76,87 @@
|
|||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
<script src="interfaces/default/js/tables/history_table.js"></script>
|
<script src="interfaces/default/js/tables/history_table.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function () {
|
||||||
|
function loadHistoryTable(media_type) {
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
"url": "get_history",
|
url: 'get_history',
|
||||||
type: "post",
|
type: 'post',
|
||||||
data: function ( d ) {
|
data: function (d) {
|
||||||
return { 'json_data': JSON.stringify( d ) };
|
return {
|
||||||
|
'json_data': JSON.stringify(d),
|
||||||
|
'media_type': media_type
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
history_table = $('#history_table').DataTable(history_table_options);
|
history_table = $('#history_table').DataTable(history_table_options);
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
|
clearSearchButton('history_table', history_table);
|
||||||
|
|
||||||
|
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
|
||||||
|
<label class="btn btn-dark active"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-all" value="all" autocomplete="off"> All \
|
||||||
|
</label> \
|
||||||
|
<label class="btn btn-dark"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-movies" value="movie" autocomplete="off"> Movies \
|
||||||
|
</label> \
|
||||||
|
<label class="btn btn-dark"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-tv_shows" value="episode" autocomplete="off"> TV Shows \
|
||||||
|
</label> \
|
||||||
|
<label class="btn btn-dark"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-music" value="track" autocomplete="off"> Music \
|
||||||
|
</label> \
|
||||||
|
</div>');
|
||||||
|
|
||||||
|
$('#media_type-selection').on('change', function () {
|
||||||
|
$('#media_type-selection > label').removeClass('active');
|
||||||
|
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
|
||||||
|
$(selected_filter).closest('label').addClass('active');
|
||||||
|
media_type = $(selected_filter).val();
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var media_type = 'all';
|
||||||
|
loadHistoryTable(media_type);
|
||||||
|
|
||||||
|
$('#row-edit-mode').on('click', function() {
|
||||||
|
$('#row-edit-mode-alert').fadeIn(200);
|
||||||
|
|
||||||
|
if ($(this).hasClass('active')) {
|
||||||
|
if (history_to_delete.length > 0) {
|
||||||
|
$('#deleteCount').text(history_to_delete.length);
|
||||||
|
$('#confirm-modal').modal();
|
||||||
|
$('#confirm-modal').one('click', '#confirm-delete', function () {
|
||||||
|
for (var i = 0; i < history_to_delete.length; i++) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_history_rows',
|
||||||
|
data: { row_id: history_to_delete[i] },
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "History deleted";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.delete-control').each(function () {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
$('#row-edit-mode-alert').fadeOut(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
history_to_delete = [];
|
||||||
|
$('.delete-control').each(function() {
|
||||||
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<th align='left' id="started">Started</th>
|
<th align='left' id="started">Started</th>
|
||||||
<th align='left' id="stopped">Stopped</th>
|
<th align='left' id="stopped">Stopped</th>
|
||||||
<th align='left' id="friendly_name">User</th>
|
<th align='left' id="friendly_name">User</th>
|
||||||
<th align='left' id="platform">Platform</th>
|
<th align='left' id="player">Player</th>
|
||||||
<th align='left' id="title">Title</th>
|
<th align='left' id="title">Title</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
<div class="modal-footer"></div>
|
<div class="modal-footer"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||||
|
</div>
|
||||||
<script src="interfaces/default/js/tables/history_table_modal.js"></script>
|
<script src="interfaces/default/js/tables/history_table_modal.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@@ -37,12 +38,41 @@
|
|||||||
type: "post",
|
type: "post",
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return { 'json_data': JSON.stringify( d ),
|
return { 'json_data': JSON.stringify( d ),
|
||||||
|
'grouping': false,
|
||||||
'start_date': '${data}'
|
'start_date': '${data}'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
history_table = $('#history_table').DataTable(history_table_modal_options);
|
history_table = $('#history_table').DataTable(history_table_modal_options);
|
||||||
|
|
||||||
|
clearSearchButton('history_table', history_table);
|
||||||
|
|
||||||
|
// Move #info-modal to parent container
|
||||||
|
if (!($('#history-modal').next().is('#info-modal'))) {
|
||||||
|
$('#info-modal').appendTo($('#history-modal').parent());
|
||||||
|
}
|
||||||
|
$('#history-modal > #info-modal').remove();
|
||||||
|
|
||||||
|
$('#history-modal').css('z-index', '1050');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1049');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
|
||||||
|
|
||||||
|
$('#info-modal').on('show.bs.modal', function () {
|
||||||
|
// Fix position to match parent modal
|
||||||
|
var currentPadding = parseInt($('body').css('padding-right'));
|
||||||
|
$(this).children('.modal-dialog').css('left', -currentPadding / 2);
|
||||||
|
$('#history-modal').css('overflow-y', 'hidden');
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#info-modal').css('z-index', '1060');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1059');
|
||||||
|
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
$('#info-modal').on('hidden.bs.modal', function () {
|
||||||
|
$('body').addClass('modal-open');
|
||||||
|
$('#history-modal').css('overflow-y', 'auto');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
% else:
|
% else:
|
||||||
|
|||||||
@@ -9,148 +9,795 @@ Variable names: data [array]
|
|||||||
|
|
||||||
data[array_index] :: Usable parameters
|
data[array_index] :: Usable parameters
|
||||||
|
|
||||||
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'popular_tv', 'top_user' or 'top_platform'
|
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'top_movies', 'popular_tv', 'popular_movies', 'top_user' or 'top_platform'
|
||||||
|
data['stat_type'] Returns the type of the stat. Either 'total_plays' or 'total_duration'
|
||||||
data['rows'] Returns an array containing stat data
|
data['rows'] Returns an array containing stat data
|
||||||
|
|
||||||
data[array_index]['rows'] :: Usable parameters
|
data[array_index]['rows'] :: Usable parameters
|
||||||
|
|
||||||
row_id Return the db row id for a metadata item if one exists
|
row_id Return the db row id for a metadata item if one exists
|
||||||
|
|
||||||
== Only if 'stat_id' is 'top_tv' or 'popular_tv' ==
|
== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_movies' or 'popular_movies' or 'top_music' or 'popular_music' or 'last_watched' ==
|
||||||
|
thumb Return the thumb for the media item.
|
||||||
|
|
||||||
|
== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_music' or 'popular_music' ==
|
||||||
grandparent_thumb Returns location of the item's thumbnail. Use with pms_image_proxy.
|
grandparent_thumb Returns location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
rating_key Returns the unique identifier for the media item.
|
rating_key Returns the unique identifier for the media item.
|
||||||
title Returns the title for the associated stat.
|
title Returns the title for the associated stat.
|
||||||
|
|
||||||
== Only if 'stat_id' is 'top_tv' or 'top_user' or 'top_platform' ==
|
== Only if 'stat_id' is 'top_tv' or 'top_movies' or 'top_music' or 'top_user' or 'top_platform' ==
|
||||||
total_plays Returns the count for the associated stat.
|
total_plays Returns the count for the associated stat.
|
||||||
|
total_duration Returns the total duration for the associated stat.
|
||||||
|
|
||||||
== Only of 'stat_id' is 'popular_tv' ==
|
== Only of 'stat_id' is 'popular_tv' or 'popular_movies' or 'popular_music' ==
|
||||||
users_watched Returns the count for the associated stat.
|
users_watched Returns the count for the associated stat.
|
||||||
|
|
||||||
== Only if 'stat_id' is 'top_user' ==
|
== Only if 'stat_id' is 'top_user' or 'last_watched' ==
|
||||||
thumb Returns url of the user's gravatar. Returns '' if none exists.
|
user_thumb Returns url of the user's gravatar. Returns '' if none exists.
|
||||||
user Returns the username for the associated stat.
|
user Returns the username for the associated stat.
|
||||||
user_id Returns the user id for the associated stat.
|
user_id Returns the user id for the associated stat.
|
||||||
friendly_name Returns the friendly name of the user for the associated stat.
|
friendly_name Returns the friendly name of the user for the associated stat.
|
||||||
|
|
||||||
== Only if 'stat_id' is 'top_platform' ==
|
== Only if 'stat_id' is 'top_platform' or 'last_watched' ==
|
||||||
platform_type Returns the platform name for the associated stat.
|
player Returns the player name for the associated stat.
|
||||||
|
|
||||||
|
== Only if 'stat_id' is 'last_watched' ==
|
||||||
|
last_watch Returns the time the media item was last watched.
|
||||||
|
|
||||||
DOCUMENTATION :: END
|
DOCUMENTATION :: END
|
||||||
</%doc>
|
</%doc>
|
||||||
|
|
||||||
% if data != None:
|
<%!
|
||||||
% if data[0]['rows']:
|
from plexpy import helpers
|
||||||
|
|
||||||
|
# Human readable duration
|
||||||
|
def hd(seconds):
|
||||||
|
minutes = helpers.cast_to_float(seconds) / 60
|
||||||
|
if minutes > 60:
|
||||||
|
hours = int(minutes / 60)
|
||||||
|
minutes = int(minutes % 60)
|
||||||
|
if minutes > 0:
|
||||||
|
return "<h3>" + str(hours) + "</h3><p>hrs</p><h3>" + str(minutes) + "</h3><p>mins</p>"
|
||||||
|
else:
|
||||||
|
return "<h3>" + str(hours) + "</h3><p>hrs</p>"
|
||||||
|
else:
|
||||||
|
return "<h3>" + str(int(minutes)) + "</h3><p>mins</p>"
|
||||||
|
%>
|
||||||
|
|
||||||
|
% if data:
|
||||||
|
% if data[0]['stat_id']:
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
% for a in data:
|
% for top_stat in data:
|
||||||
% if a['stat_id'] == 'top_tv':
|
% if top_stat['stat_id'] == 'top_tv' and top_stat['rows']:
|
||||||
<div class="home-platforms-instance">
|
<div class="home-platforms-instance">
|
||||||
<li>
|
<li>
|
||||||
<span>
|
<div class="home-platforms-instance-info">
|
||||||
<a href="info?item_id=${a['rows'][0]['rating_key']}">
|
|
||||||
% if a['rows'][0]['grandparent_thumb']:
|
|
||||||
<img class="home-platforms-instance-poster"
|
|
||||||
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
|
|
||||||
% else:
|
|
||||||
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
|
|
||||||
% endif
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<div class="home-platforms-instance-name">
|
<div class="home-platforms-instance-name">
|
||||||
<h4>Most Watched TV</h4>
|
<h4>Most Watched TV</h4>
|
||||||
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
|
|
||||||
${a['rows'][0]['title']}
|
|
||||||
</a></h5>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-platforms-instance-playcount">
|
<div class="home-platforms-instance-playcount">
|
||||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
<h4>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][0]['total_plays']}</h3>
|
||||||
<p> plays</p>
|
<p> plays</p>
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
% elif a['stat_id'] == 'popular_tv':
|
|
||||||
<div class="home-platforms-instance">
|
|
||||||
<li>
|
|
||||||
<span>
|
|
||||||
<a href="info?item_id=${a['rows'][0]['rating_key']}">
|
|
||||||
% if a['rows'][0]['grandparent_thumb'] != '':
|
|
||||||
<img class="home-platforms-instance-poster"
|
|
||||||
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
|
|
||||||
% else:
|
% else:
|
||||||
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
|
${top_stat['rows'][0]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['grandparent_thumb']:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
% endif
|
% endif
|
||||||
</a>
|
</a>
|
||||||
</span>
|
%if len(top_stat['rows']) > 1:
|
||||||
<div class="home-platforms-instance-name">
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
<h4>Most Popular TV</h4>
|
<ul class="list-unstyled">
|
||||||
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
|
<div class="slider">
|
||||||
${a['rows'][0]['title']}
|
<div class="home-platforms-instance-list">
|
||||||
</a></h5>
|
% for row in top_stat['rows']:
|
||||||
</div>
|
%if loop.index > 0:
|
||||||
<div class="user-platforms-instance-playcount">
|
|
||||||
<h3>${a['rows'][0]['users_watched']}</h3>
|
|
||||||
<p> users</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
% elif a['stat_id'] == 'top_users':
|
|
||||||
<div class="home-platforms-instance">
|
|
||||||
<li>
|
<li>
|
||||||
<span>
|
<div class="home-platforms-instance-list-info">
|
||||||
% if a['rows'][0]['user_id']:
|
<div class="home-platforms-instance-list-name">
|
||||||
<a href="user?user_id=${a['rows'][0]['user_id']}">
|
|
||||||
% else:
|
|
||||||
<a href="user?user=${a['rows'][0]['user']}">
|
|
||||||
% endif
|
|
||||||
% if a['rows'][0]['thumb'] != '':
|
|
||||||
<img class="home-platforms-instance-oval" src="${a['rows'][0]['thumb']}"
|
|
||||||
class="poster-face">
|
|
||||||
% else:
|
|
||||||
<img class="home-platforms-instance-oval"
|
|
||||||
src="interfaces/default/images/gravatar-default.png">
|
|
||||||
% endif
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<div class="home-platforms-instance-name">
|
|
||||||
<h4>Most Active User</h4>
|
|
||||||
<h5>
|
<h5>
|
||||||
% if a['rows'][0]['user_id']:
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
<a href="user?user_id=${a['rows'][0]['user_id']}">
|
${top_stat['rows'][loop.index]['title']}
|
||||||
% else:
|
|
||||||
<a href="user?user=${a['rows'][0]['user']}">
|
|
||||||
% endif
|
|
||||||
${a['rows'][0]['friendly_name']}
|
|
||||||
</a>
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-platforms-instance-playcount">
|
<div class="home-platforms-instance-list-playcount">
|
||||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
|
||||||
<p> plays</p>
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][loop.index]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['grandparent_thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
</div>
|
</div>
|
||||||
% elif a['stat_id'] == 'top_platforms':
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'popular_tv' and top_stat['rows']:
|
||||||
<div class="home-platforms-instance">
|
<div class="home-platforms-instance">
|
||||||
<li>
|
<li>
|
||||||
<div id="platform-stat">
|
<div class="home-platforms-instance-info">
|
||||||
<img class="home-platforms-instance-box" src="interfaces/default/images/platforms/default.png">
|
|
||||||
</div>
|
|
||||||
<div class="home-platforms-instance-name">
|
<div class="home-platforms-instance-name">
|
||||||
<h4>Most Active Platform</h4>
|
<h4>Most Popular TV</h4>
|
||||||
<h5>${a['rows'][0]['platform_type']}</h5>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-platforms-instance-playcount">
|
<div class="home-platforms-instance-playcount">
|
||||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
<h4>
|
||||||
<p> plays</p>
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h3>${top_stat['rows'][0]['users_watched']}</h3>
|
||||||
|
<p> users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['grandparent_thumb'] != '':
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
${top_stat['rows'][loop.index]['title']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
<h3>${top_stat['rows'][loop.index]['users_watched']}</h3>
|
||||||
|
<p> users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['grandparent_thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'top_movies' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Most Watched Movie</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h4>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][0]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][0]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['thumb']:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
${top_stat['rows'][loop.index]['title']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][loop.index]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'popular_movies' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Most Popular Movie</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h4>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h3>${top_stat['rows'][0]['users_watched']}</h3>
|
||||||
|
<p> users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['thumb']:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
${top_stat['rows'][loop.index]['title']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
<h3>${top_stat['rows'][loop.index]['users_watched']}</h3>
|
||||||
|
<p> users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'top_music' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Most Listened to Artist</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h4>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][0]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][0]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['grandparent_thumb']:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
${top_stat['rows'][loop.index]['title']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][loop.index]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['grandparent_thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'popular_music' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Most Popular Artist</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h4>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h3>${top_stat['rows'][0]['users_watched']}</h3>
|
||||||
|
<p> users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['grandparent_thumb'] != '':
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
${top_stat['rows'][loop.index]['title']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
<h3>${top_stat['rows'][loop.index]['users_watched']}</h3>
|
||||||
|
<p> users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['grandparent_thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=300&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'top_users' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Most Active User</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h4>
|
||||||
|
% if top_stat['rows'][0]['user_id']:
|
||||||
|
<a href="user?user_id=${top_stat['rows'][0]['user_id']}" title="${top_stat['rows'][0]['friendly_name']}">
|
||||||
|
% else:
|
||||||
|
<a href="user?user=${top_stat['rows'][0]['user']}" title="${top_stat['rows'][0]['friendly_name']}">
|
||||||
|
% endif
|
||||||
|
${top_stat['rows'][0]['friendly_name']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][0]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][0]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if top_stat['rows'][0]['user_id']:
|
||||||
|
<a href="user?user_id=${top_stat['rows'][0]['user_id']}" title="${top_stat['rows'][0]['friendly_name']}">
|
||||||
|
% else:
|
||||||
|
<a href="user?user=${top_stat['rows'][0]['user']}" title="${top_stat['rows'][0]['friendly_name']}">
|
||||||
|
% endif
|
||||||
|
% if top_stat['rows'][0]['user_thumb'] != '':
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-instance-oval" style="background-image: url(${top_stat['rows'][0]['user_thumb']});"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-instance-oval" style="background-image: url(interfaces/default/images/gravatar-default.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
% if top_stat['rows'][loop.index]['user_id']:
|
||||||
|
<a href="user?user_id=${top_stat['rows'][loop.index]['user_id']}" title="${top_stat['rows'][loop.index]['friendly_name']}">
|
||||||
|
% else:
|
||||||
|
<a href="user?user=${top_stat['rows'][loop.index]['user']}" title="${top_stat['rows'][loop.index]['friendly_name']}">
|
||||||
|
% endif
|
||||||
|
${top_stat['rows'][loop.index]['friendly_name']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][loop.index]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if top_stat['rows'][loop.index]['user_id']:
|
||||||
|
<a href="user?user_id=${top_stat['rows'][loop.index]['user_id']}" title="${top_stat['rows'][loop.index]['friendly_name']}">
|
||||||
|
% else:
|
||||||
|
<a href="user?user=${top_stat['rows'][loop.index]['user']}" title="${top_stat['rows'][loop.index]['friendly_name']}">
|
||||||
|
% endif
|
||||||
|
% if top_stat['rows'][loop.index]['user_thumb'] != '':
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-instance-list-oval" style="background-image: url(${top_stat['rows'][loop.index]['user_thumb']});"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-instance-list-oval" style="background-image: url(interfaces/default/images/gravatar-default.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'top_platforms' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Most Active Platform</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h4 title="${top_stat['rows'][0]['platform_type']}">${top_stat['rows'][0]['platform_type']}</h4>
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][0]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][0]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="platform-stat" class="home-platforms-instance-poster" title="${top_stat['rows'][0]['platform_type']}">
|
||||||
<script>
|
<script>
|
||||||
$("#platform-stat").html("<img class='home-platforms-instance-box' src='" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + "'>");
|
$("#platform-stat").html("<div class='home-platforms-instance-box' style='background-image: url(" + getPlatformImagePath('${top_stat['rows'][0]['platform_type']}') + ");'>");
|
||||||
</script>
|
</script>
|
||||||
|
</div>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5 title="${top_stat['rows'][loop.index]['platform_type']}">
|
||||||
|
${top_stat['rows'][loop.index]['platform_type']}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-playcount">
|
||||||
|
% if top_stat['stat_type'] == 'total_plays':
|
||||||
|
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
|
||||||
|
<p> plays</p>
|
||||||
|
% else:
|
||||||
|
${top_stat['rows'][loop.index]['total_duration'] | hd}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-poster" id="home-platforms-instance-poster-${loop.index + 1}" title="${top_stat['rows'][loop.index]['platform_type']}">
|
||||||
|
<script>
|
||||||
|
$("#home-platforms-instance-poster-${loop.index + 1}").html("<div class='home-platforms-instance-list-box' style='background-image: url(" + getPlatformImagePath('${top_stat['rows'][loop.index]['platform_type']}') + ");'>");
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% elif top_stat['stat_id'] == 'last_watched' and top_stat['rows']:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
<h4>Last Watched</h4>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-last-user">
|
||||||
|
<h4>
|
||||||
|
<a href="info?source=history&item_id=${top_stat['rows'][0]['row_id']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
${top_stat['rows'][0]['title']}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<h5>
|
||||||
|
% if top_stat['rows'][0]['user_id']:
|
||||||
|
<a href="user?user_id=${top_stat['rows'][0]['user_id']}" title="${top_stat['rows'][0]['friendly_name']}">
|
||||||
|
% else:
|
||||||
|
<a href="user?user=${top_stat['rows'][0]['user']}" title="${top_stat['rows'][0]['friendly_name']}">
|
||||||
|
% endif
|
||||||
|
${top_stat['rows'][0]['friendly_name']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
<span id="last-watch-stat">
|
||||||
|
<script>
|
||||||
|
$('#last-watch-stat').text(moment(${top_stat['rows'][0]['last_watch']},"X").format(date_format));
|
||||||
|
</script>
|
||||||
|
</span> - ${top_stat['rows'][0]['player']}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?source=history&item_id=${top_stat['rows'][0]['row_id']}" title="${top_stat['rows'][0]['title']}">
|
||||||
|
% if top_stat['rows'][0]['thumb']:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
%if len(top_stat['rows']) > 1:
|
||||||
|
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="home-platforms-instance-list">
|
||||||
|
% for row in top_stat['rows']:
|
||||||
|
%if loop.index > 0:
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-list-info">
|
||||||
|
<div class="home-platforms-instance-list-name">
|
||||||
|
<h5>
|
||||||
|
<a href="info?source=history&item_id=${top_stat['rows'][loop.index]['row_id']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
${top_stat['rows'][loop.index]['title']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-list-last-user">
|
||||||
|
<h5>
|
||||||
|
% if top_stat['rows'][loop.index]['user_id']:
|
||||||
|
<a href="user?user_id=${top_stat['rows'][loop.index]['user_id']}" title="${top_stat['rows'][loop.index]['friendly_name']}">
|
||||||
|
% else:
|
||||||
|
<a href="user?user=${top_stat['rows'][loop.index]['user']}" title="${top_stat['rows'][loop.index]['friendly_name']}">
|
||||||
|
% endif
|
||||||
|
${top_stat['rows'][loop.index]['friendly_name']}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
<span id="home-platforms-instance-list-last-watch-${loop.index + 1}">
|
||||||
|
<script>
|
||||||
|
$('#home-platforms-instance-list-last-watch-${loop.index + 1}').text(moment(${top_stat['rows'][loop.index]['last_watch']},"X").format(date_format));
|
||||||
|
</script>
|
||||||
|
</span> - ${top_stat['rows'][loop.index]['player']}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="info?source=history&item_id=${top_stat['rows'][loop.index]['row_id']}" title="${top_stat['rows'][loop.index]['title']}">
|
||||||
|
% if top_stat['rows'][loop.index]['thumb']:
|
||||||
|
<div class="home-platforms-instance-list-poster">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster2">
|
||||||
|
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
<div class="home-platforms-instance-list-number">
|
||||||
|
<h4>${loop.index + 1}</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
|
<script>
|
||||||
|
var topZIndex = 2;
|
||||||
|
$('.home-platforms-instance-list-chevron').on('click', function() {
|
||||||
|
var instanceBoxChevron = $(this);
|
||||||
|
var instanceBox = $(this).parents('.home-platforms-instance');
|
||||||
|
var instanceBoxSlider = instanceBox.find('.slider');
|
||||||
|
|
||||||
|
topZIndex++;
|
||||||
|
instanceBoxChevron.toggleClass('active');
|
||||||
|
instanceBoxSlider.css('z-index', topZIndex);
|
||||||
|
instanceBoxSlider.stop().slideToggle(500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
% else:
|
% else:
|
||||||
<div class="text-muted">No stats for selected period.</div><br>
|
<div class="text-muted">No stats for selected period.</div><br>
|
||||||
% endif
|
% endif
|
||||||
|
|||||||
BIN
data/interfaces/default/images/platforms/msedge.png
Normal file
BIN
data/interfaces/default/images/platforms/msedge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
data/interfaces/default/images/platforms/pmp.png
Normal file
BIN
data/interfaces/default/images/platforms/pmp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
data/interfaces/default/images/plex-logo-light-small.png
Normal file
BIN
data/interfaces/default/images/plex-logo-light-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
26
data/interfaces/default/images/plex-logo-light.svg
Normal file
26
data/interfaces/default/images/plex-logo-light.svg
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="3086px" height="1000px" viewBox="0 0 3086 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||||
|
<!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>plex-logo-light</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<radialGradient cx="89.2670157%" cy="49.76%" fx="89.2670157%" fy="49.76%" r="92.4996161%" id="radialGradient-1">
|
||||||
|
<stop stop-color="#F9BE03" offset="0%"></stop>
|
||||||
|
<stop stop-color="#CC7C19" offset="100%"></stop>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||||
|
<g id="plex-logo-light" sketch:type="MSArtboardGroup">
|
||||||
|
<g sketch:type="MSLayerGroup">
|
||||||
|
<path d="M3085.99,0 L2795.989,0 L2505.99,500 L2795.989,1000 L3085.737,1000 L2795.989,500.25 L3085.99,0" id="X" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
|
||||||
|
<path d="M2186,0 L2476.00071,0 L2796,500.25 L2476.00071,1000.5 L2186,1000.5 L2505.99929,500.25 L2186,0" id="chevron" fill="url(#radialGradient-1)" sketch:type="MSShapeGroup"></path>
|
||||||
|
<path d="M2085.947,1000 L1508.874,1000 L1508.874,0 L2085.947,0 L2085.947,173.737 L1721.339,173.737 L1721.339,393.299 L2060.594,393.299 L2060.594,567.03 L1721.339,567.03 L1721.339,824.895 L2085.947,824.895 L2085.947,1000" id="E" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
|
||||||
|
<path d="M791.276,1000 L791.276,0 L1003.316,0 L1003.316,824.895 L1408.925,824.895 L1408.925,1000 L791.276,1000" id="L" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
|
||||||
|
<g id="P" fill="#FFFFFF" sketch:type="MSShapeGroup">
|
||||||
|
<path d="M589.947,558.824 C522.679,615.831 427.037,644.325 303.009,644.325 L212.04,644.325 L212.04,1000 L0,1000 L0,643.947829 L0,470.337388 L290,470.697418 C467.563171,468.627777 476.842468,359.878967 476.842468,322.200012 C476.842468,287.221283 476.842468,175.445374 319,173.699997 L0,173.703242 L0,0 L319.424,0 C440.717,0 532.939,26.107 596.101,78.321 C659.253,130.534 690.834,208.392 690.834,311.902 C690.834,419.527 657.202,501.83 589.947,558.824 Z" id="Path"></path>
|
||||||
|
<rect id="Path" x="0" y="110" width="212.2" height="429"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -16,17 +16,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
% if config['home_stats_cards'] > 'watch_statistics':
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
<h3>Statistics <small>Last 30 days</small></h3>
|
<h3>Watch Statistics <small>Last ${config['home_stats_length']} days</small></h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="home-stats" class="user-platforms">
|
<div id="home-stats" class="home-platforms">
|
||||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
% endif
|
||||||
|
% if config['home_library_cards'] > 'library_statistics':
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="padded-header" id="library-statistics-header">
|
||||||
|
<h3>Library Statistics <small>${config['pms_name']}</small></h3>
|
||||||
|
</div>
|
||||||
|
<div id="library-stats" class="library-platforms">
|
||||||
|
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="padded-header">
|
<div class="padded-header">
|
||||||
@@ -45,17 +60,18 @@
|
|||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
function getHomeStats(days) {
|
function currentActivityHeader() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'home_stats',
|
url: 'get_current_activity_header',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
data: {time_range: days},
|
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#home-stats").html(xhr.responseText);
|
$("#current-activity-header").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
currentActivityHeader();
|
||||||
|
setInterval(currentActivityHeader, 15000);
|
||||||
|
|
||||||
function currentActivity() {
|
function currentActivity() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@@ -70,18 +86,29 @@
|
|||||||
currentActivity();
|
currentActivity();
|
||||||
setInterval(currentActivity, 15000);
|
setInterval(currentActivity, 15000);
|
||||||
|
|
||||||
function currentActivityHeader() {
|
function getHomeStats(days) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_current_activity_header',
|
url: 'home_stats',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
|
data: { },
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#current-activity-header").html(xhr.responseText);
|
$("#home-stats").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLibraryStats() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'library_stats',
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
data: { },
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#library-stats").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
currentActivityHeader();
|
|
||||||
setInterval(currentActivityHeader, 15000);
|
|
||||||
|
|
||||||
function recentlyAdded() {
|
function recentlyAdded() {
|
||||||
var widthVal = $('body').find(".container-fluid").width();
|
var widthVal = $('body').find(".container-fluid").width();
|
||||||
@@ -110,7 +137,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
getHomeStats(30);
|
var date_format = 'YYYY-MM-DD';
|
||||||
|
var time_format = 'hh:mm a';
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_date_formats',
|
||||||
|
type: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
date_format = data.date_format;
|
||||||
|
time_format = data.time_format;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getHomeStats();
|
||||||
|
getLibraryStats();
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,25 +11,39 @@ data :: Usable parameters (if not applicable for media type, blank value will be
|
|||||||
|
|
||||||
== Global keys ==
|
== Global keys ==
|
||||||
rating_key Returns the unique identifier for the media item.
|
rating_key Returns the unique identifier for the media item.
|
||||||
type Returns the type of media. Either 'movie', 'episode' or 'show' or 'season'.
|
media_type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'.
|
||||||
art Returns the location of the item's artwork
|
art Returns the location of the item's artwork
|
||||||
title Returns the name of the episode, show, season or movie.
|
title Returns the name of the movie, show, episode, artist, album, or track.
|
||||||
duration Returns the standard runtime of the media.
|
duration Returns the standard runtime of the media.
|
||||||
content_rating Returns the age rating for the media.
|
content_rating Returns the age rating for the media.
|
||||||
summary Returns a brief description of the media plot.
|
summary Returns a brief description of the media plot.
|
||||||
grandparent_title Returns the name of the TV show.
|
grandparent_title Returns the name of the show, or artist.
|
||||||
parent_index Returns the season number of the TV show.
|
parent_index Returns the index number of the season.
|
||||||
index Returns the episode number.
|
index Returns the index number of the episode, or track.
|
||||||
parent_thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
parent_thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
writers Returns an array of writers.
|
writers Returns an array of writers.
|
||||||
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
parent_title Returns the name of the TV show.
|
parent_title Returns the name of the show, or artist.
|
||||||
rating Returns the 5 star rating value for the movie. Between 1 and 5.
|
rating Returns the 5 star rating value for the movie. Between 1 and 5.
|
||||||
year Returns the release year of the movie.
|
year Returns the release year of the movie, or show.
|
||||||
genres Returns an array of genres.
|
genres Returns an array of genres.
|
||||||
actors Returns an array of actors.
|
actors Returns an array of actors.
|
||||||
directors Returns an array of directors.
|
directors Returns an array of directors.
|
||||||
studio Returns the name of the studio.
|
studio Returns the name of the studio.
|
||||||
|
originally_available_at Returns the air date of the item.
|
||||||
|
|
||||||
|
query :: Usable parameters
|
||||||
|
|
||||||
|
== Global keys ==
|
||||||
|
query_string Returns the string used for the search query.
|
||||||
|
title Returns the name of the movie, episode, or track.
|
||||||
|
parent_title Returns the name of the album.
|
||||||
|
grandparent_title Returns the name of the show, or artist.
|
||||||
|
media_index Returns the index number of the episode, or track.
|
||||||
|
parent_media_index Returns the index number of the season.
|
||||||
|
year Returns the release year of the movie, or show.
|
||||||
|
media_type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'.
|
||||||
|
rating_key Returns the unique identifier for the media item.
|
||||||
|
|
||||||
DOCUMENTATION :: END
|
DOCUMENTATION :: END
|
||||||
</%doc>
|
</%doc>
|
||||||
@@ -46,63 +60,169 @@ DOCUMENTATION :: END
|
|||||||
% if data:
|
% if data:
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div>
|
% if data['media_type'] != 'library':
|
||||||
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)">
|
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
|
||||||
<div class="summary-wrapper">
|
% endif
|
||||||
<div class="summary-overlay">
|
<div class="summary-container">
|
||||||
<div class="row">
|
<div class="summary-navbar">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="summary-navbar-list">
|
||||||
|
<ul class="list-unstyled breadcrumb">
|
||||||
|
% if data['media_type'] == 'library':
|
||||||
|
% if data['library'] == 'movie':
|
||||||
|
<li class="active">Movies</li>
|
||||||
|
% elif data['library'] == 'show':
|
||||||
|
<li class="active">TV Shows</li>
|
||||||
|
% elif data['library'] == 'artist':
|
||||||
|
<li class="active">Music</li>
|
||||||
|
% endif
|
||||||
|
% elif data['media_type'] == 'movie':
|
||||||
|
<li><a href="info?item_id=movie">Movies</a></li>
|
||||||
|
<li class="active">${data['title']}</li>
|
||||||
|
% elif data['media_type'] == 'show':
|
||||||
|
<li><a href="info?item_id=show">TV Shows</a></li>
|
||||||
|
<li class="active">${data['title']}</li>
|
||||||
|
% elif data['media_type'] == 'season':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
|
||||||
|
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||||
|
<li class="active">Season ${data['index']}</li>
|
||||||
|
% elif data['media_type'] == 'episode':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
||||||
|
<li><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></li>
|
||||||
|
<li class="active">Episode ${data['index']} - ${data['title']}</li>
|
||||||
|
% elif data['media_type'] == 'artist':
|
||||||
|
<li><a href="info?item_id=artist">Music</a></li>
|
||||||
|
<li class="active">${data['title']}</li>
|
||||||
|
% elif data['media_type'] == 'album':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
|
||||||
|
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||||
|
<li class="active">${data['title']}</li>
|
||||||
|
% elif data['media_type'] == 'track':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
||||||
|
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||||
|
<li class="active">Track ${data['index']} - ${data['title']}</li>
|
||||||
|
% endif
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if data['media_type'] != 'library':
|
||||||
|
<div class="summary-content-title-wrapper">
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||||
% if data['type'] == 'episode':
|
% if data['media_type'] == 'track':
|
||||||
<img src="pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster">
|
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web">
|
||||||
% else:
|
% elif data['media_type'] != 'library':
|
||||||
<img src="pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster">
|
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web">
|
||||||
% endif
|
% endif
|
||||||
|
% if data['media_type'] == 'episode':
|
||||||
|
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=poster);">
|
||||||
|
<div class="summary-poster-face-overlay">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||||
|
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=poster);">
|
||||||
|
<div class="summary-poster-face-overlay">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif data['media_type'] != 'library':
|
||||||
|
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
|
||||||
|
<div class="summary-poster-face-overlay">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-content">
|
|
||||||
<div class="summary-content-title">
|
<div class="summary-content-title">
|
||||||
% if data['type'] == 'movie':
|
% if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'artist':
|
||||||
<h1>${data['title']} (${data['year']})</h1>
|
<h1> </h1><h1>${data['title']}</h1>
|
||||||
% elif data['type'] == 'season':
|
% elif data['media_type'] == 'season':
|
||||||
<h1>${data['parent_title']} (${data['title']})</h1>
|
<h1> </h1><h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||||
% elif data['type'] == 'episode':
|
<h3 class="hidden-xs">S${data['index']}</h3>
|
||||||
<h1>${data['grandparent_title']} (Season ${data['parent_index']}, Episode
|
% elif data['media_type'] == 'episode':
|
||||||
${data['index']}) "${data['title']}"</h1>
|
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
|
||||||
% else:
|
<h2>${data['title']}</h2>
|
||||||
<h1>${data['title']}</h1>
|
<h3 class="hidden-xs">S${data['parent_index']} · E${data['index']}</h3>
|
||||||
|
% elif data['media_type'] == 'album':
|
||||||
|
<h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||||
|
<h2>${data['title']}</h2>
|
||||||
|
% elif data['media_type'] == 'track':
|
||||||
|
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
|
||||||
|
<h2><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2>
|
||||||
|
<h3 class="hidden-xs">T${data['index']}</h3>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% if data['type'] == 'movie':
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
<div class="summary-content-wrapper">
|
||||||
|
% if data['media_type'] != 'library':
|
||||||
|
<div class="col-md-9">
|
||||||
|
% if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'season':
|
||||||
|
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div>
|
||||||
|
% elif data['media_type'] == 'episode':
|
||||||
|
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 40px;"></div>
|
||||||
|
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||||
|
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;"></div>
|
||||||
|
% else:
|
||||||
|
<div class="summary-content-padding hidden-xs hidden-sm"></div>
|
||||||
|
% endif
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="summary-content-details-wrapper">
|
||||||
|
% if data['rating']:
|
||||||
<div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
|
<div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
|
||||||
data-rateit-ispreset="true" data-rateit-readonly="true"></div>
|
data-rateit-ispreset="true" data-rateit-readonly="true"></div>
|
||||||
% endif
|
% endif
|
||||||
<div class="summary-content-details-wrapper">
|
<div class="summary-content-details-tag">
|
||||||
<div class="summary-content-director">
|
|
||||||
% if data['type'] == 'episode' or data['type'] == 'movie':
|
|
||||||
% if data['directors']:
|
% if data['directors']:
|
||||||
Directed by <strong> ${data['directors'][0]}</strong>
|
Directed by <strong> ${data['directors'][0]}</strong>
|
||||||
% else:
|
|
||||||
Directed by <strong> unknown</strong>
|
|
||||||
% endif
|
% endif
|
||||||
% elif data['type'] == 'show' or data['type'] == 'season':
|
</div>
|
||||||
|
<div class="summary-content-details-tag">
|
||||||
|
% if data['studio']:
|
||||||
Studio <strong> ${data['studio']}</strong>
|
Studio <strong> ${data['studio']}</strong>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-content-duration">
|
<div class="summary-content-details-tag">
|
||||||
|
% if data['media_type'] == 'movie':
|
||||||
|
Year <strong> ${data['year']}</strong>
|
||||||
|
% elif data['media_type'] == 'show':
|
||||||
|
Aired <strong> ${data['year']}</strong>
|
||||||
|
% elif data['media_type'] == 'episode':
|
||||||
|
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
||||||
|
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||||
|
Released <strong> ${data['year']}</strong>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-details-tag">
|
||||||
|
% if data['duration']:
|
||||||
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
|
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-content-content-rating">
|
<div class="summary-content-details-tag">
|
||||||
|
% if data['content_rating']:
|
||||||
Rated <strong> ${data['content_rating']} </strong>
|
Rated <strong> ${data['content_rating']} </strong>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
% if data['tagline']:
|
||||||
|
<div class="summary-content-summary">
|
||||||
|
<p><strong> ${data['tagline']} </strong></p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
<div class="summary-content-summary">
|
<div class="summary-content-summary">
|
||||||
<p> ${data['summary']} </p>
|
<p> ${data['summary']} </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% if data['type'] == 'episode':
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
||||||
|
% if data['writers']:
|
||||||
<div class="summary-content-writers">
|
<div class="summary-content-writers">
|
||||||
<strong>Written by</strong>
|
<strong>Written by</strong>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -115,24 +235,8 @@ DOCUMENTATION :: END
|
|||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
% elif data['type'] == 'movie' or data['type'] == 'show':
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
|
||||||
<div class="summary-content-actors">
|
|
||||||
<strong>Genres</strong>
|
|
||||||
<ul>
|
|
||||||
% for genre in data['genres']:
|
|
||||||
% if loop.index < 5:
|
|
||||||
<li>
|
|
||||||
${genre}
|
|
||||||
</li>
|
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% if data['actors']:
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
|
||||||
<div class="summary-content-actors">
|
<div class="summary-content-actors">
|
||||||
<strong>Starring</strong>
|
<strong>Starring</strong>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -145,79 +249,240 @@ DOCUMENTATION :: END
|
|||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
|
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
||||||
|
% if data['genres']:
|
||||||
|
<div class="summary-content-genres">
|
||||||
|
<strong>Genres</strong>
|
||||||
|
<ul>
|
||||||
|
% for genre in data['genres']:
|
||||||
|
% if loop.index < 5:
|
||||||
|
<li>
|
||||||
|
${genre}
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
% elif data['type'] == 'season':
|
|
||||||
<div class="col-md-3"></div>
|
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
% if data['media_type'] == 'show':
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show':
|
|
||||||
<div class='container-fluid'>
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-md-12'>
|
<div class='col-md-12'>
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span>Watch history for <strong>${data['title']}</strong></span>
|
<span>Season List for <strong>${data['title']}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="colvis-button-bar hidden-xs">
|
</div>
|
||||||
|
<div class='table-card-back'>
|
||||||
|
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading season list...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif data['media_type'] == 'season':
|
||||||
|
<div class='col-md-12'>
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>Episode List for <strong>${data['title']}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='table-card-back'>
|
||||||
|
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading episode list...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif data['media_type'] == 'artist':
|
||||||
|
<div class='col-md-12'>
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>Album List for <strong>${data['title']}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='table-card-back'>
|
||||||
|
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading album list...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif data['media_type'] == 'album':
|
||||||
|
<div class='col-md-12'>
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>Track List for <strong>${data['title']}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='table-card-back'>
|
||||||
|
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading track list...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
<div class='col-md-12'>
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>Watch History for <strong>${data['title']}</strong></span>
|
||||||
|
</div>
|
||||||
|
<div class="button-bar">
|
||||||
|
<div class="colvis-button-bar hidden-xs"></div>
|
||||||
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||||
|
<i class="fa fa-trash-o"></i> Delete mode
|
||||||
|
</button>
|
||||||
|
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select rows to delete. Data is deleted upon exiting delete mode.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<table class="display" id="history_table" width="100%">
|
<table class="display" id="history_table" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th align='left' id="delete">Delete</th>
|
||||||
<th align='left' id="time">Time</th>
|
<th align='left' id="time">Time</th>
|
||||||
<th align='left' id="friendly_name">User</th>
|
<th align='left' id="friendly_name">User</th>
|
||||||
<th align='left' id="platform">Platform</th>
|
|
||||||
<th align='left' id="ip_address">IP Address</th>
|
<th align='left' id="ip_address">IP Address</th>
|
||||||
|
<th align='left' id="platform">Platform</th>
|
||||||
|
<th align='left' id="player">Player</th>
|
||||||
<th align='left' id="title">Title</th>
|
<th align='left' id="title">Title</th>
|
||||||
<th align='left' id="started">Started</th>
|
<th align='left' id="started">Started</th>
|
||||||
<th align='left' id="paused_counter">Paused</th>
|
<th align='left' id="paused_counter">Paused</th>
|
||||||
<th align='left' id="stopped">Stopped</th>
|
<th align='left' id="stopped">Stopped</th>
|
||||||
<th align='left' id="duration">Duration</th>
|
<th align='left' id="duration">Duration</th>
|
||||||
<th align='left' id="percent_complete">Watched</th>
|
<th align='left' id="percent_complete"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody></tbody>
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Confirm Delete</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
|
||||||
|
<p>This is permanent and cannot be undone!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
% elif data['type'] == 'season':
|
|
||||||
<div class='container-fluid'>
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-md-12'>
|
|
||||||
<div class='table-card-header'>
|
|
||||||
<div class="header-bar">
|
|
||||||
<span>Episode list for <strong>${data['title']}</strong></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='table-card-back'>
|
|
||||||
<div id="episode-list"></div>
|
|
||||||
</div>
|
</div>
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-10">
|
<div class="summary-container">
|
||||||
<h3>Error retrieving item data. This media may not be available in the Plex Media Server database
|
<div class="summary-navbar">
|
||||||
anymore.</h3>
|
<div class="col-md-12">
|
||||||
|
<div class="summary-navbar-list">
|
||||||
|
<ul class="list-unstyled breadcrumb">
|
||||||
|
% if query:
|
||||||
|
% if query['media_type'] == 'movie':
|
||||||
|
<li><a href="info?item_id=movie">Movies</a></li>
|
||||||
|
<li class="active">${query['title']}</li>
|
||||||
|
% elif query['media_type'] == 'show':
|
||||||
|
<li><a href="info?item_id=show">TV Shows</a></li>
|
||||||
|
<li class="active">${query['grandparent_title']}</li>
|
||||||
|
% elif query['media_type'] == 'season':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
|
||||||
|
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
|
||||||
|
<li class="active">Season ${query['parent_media_index']}</li>
|
||||||
|
% elif query['media_type'] == 'episode':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
|
||||||
|
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
|
||||||
|
<li>Season ${query['parent_media_index']}</li>
|
||||||
|
<li class="active">Episode ${query['media_index']} - ${query['title']}</li>
|
||||||
|
% elif query['media_type'] == 'artist':
|
||||||
|
<li><a href="info?item_id=artist">Music</a></li>
|
||||||
|
<li class="active">${query['grandparent_title']}</li>
|
||||||
|
% elif query['media_type'] == 'album':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
|
||||||
|
<li>${query['grandparent_title']}</li>
|
||||||
|
<li class="active">${query['parent_title']}</li>
|
||||||
|
% elif query['media_type'] == 'track':
|
||||||
|
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
|
||||||
|
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
|
||||||
|
<li>${query['parent_title']}</li>
|
||||||
|
<li class="active">Track ${query['media_index']} - ${query['title']}</li>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-title-wrapper">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h4 style="text-align: center; margin-bottom: 20px;">
|
||||||
|
Error retrieving item metadata. This media item is not available in the Plex Media Server library.
|
||||||
|
</h4>
|
||||||
|
% if query:
|
||||||
|
<h4 style="text-align: center; margin-bottom: 20px;">
|
||||||
|
If the item has been moved, please select the correct match below to update the PlexPy database.
|
||||||
|
</h4>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-wrapper">
|
||||||
|
<div class='col-md-12'>
|
||||||
|
% if query:
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span>Search Results for <strong>${query['query_string']}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='table-card-back'>
|
||||||
|
<div id="search-results-list"><i class="fa fa-refresh fa-spin"></i> Loading search results...</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Confirm Update</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<p>Are you REALLY sure you want to replace
|
||||||
|
<p><strong>
|
||||||
|
% if query['media_type'] == 'movie':
|
||||||
|
${query['title']}<br />${query['year']}
|
||||||
|
% elif query['media_type'] == 'show':
|
||||||
|
${query['grandparent_title']}
|
||||||
|
% elif query['media_type'] == 'season':
|
||||||
|
${query['grandparent_title']}<br />S${query['parent_media_index']}
|
||||||
|
% elif query['media_type'] == 'episode':
|
||||||
|
${query['grandparent_title']}<br />${query['title']}<br />S${query['parent_media_index']} · E${query['media_index']}
|
||||||
|
% elif query['media_type'] == 'artist':
|
||||||
|
${query['grandparent_title']}
|
||||||
|
% elif query['media_type'] == 'album':
|
||||||
|
${query['grandparent_title']}<br />${query['parent_title']}
|
||||||
|
% elif query['media_type'] == 'track':
|
||||||
|
${query['grandparent_title']}<br />${query['title']}<br />${query['parent_title']}
|
||||||
|
% endif
|
||||||
|
</strong></p>
|
||||||
|
<p> with </p>
|
||||||
|
<p><span id="new_title"></span></p>
|
||||||
|
% if query['media_type'] != 'movie':
|
||||||
|
<p>All items for <strong>${query['grandparent_title']}</strong> will also be updated.</p>
|
||||||
|
% endif
|
||||||
|
<p>This is permanent and cannot be undone!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-update">Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,68 +498,179 @@ DOCUMENTATION :: END
|
|||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
|
|
||||||
% if data:
|
% if data:
|
||||||
% if data['type'] == 'movie':
|
|
||||||
<script>
|
|
||||||
// Convert rating to 5 star rating type
|
|
||||||
var starRating = Math.round(${data['rating']} / 2)
|
|
||||||
$('#stars').attr('data-rateit-value', starRating)
|
|
||||||
</script>
|
|
||||||
% endif
|
|
||||||
% if data['type'] == 'movie' or data['type'] == 'episode':
|
|
||||||
<script src="interfaces/default/js/tables/history_table.js"></script>
|
<script src="interfaces/default/js/tables/history_table.js"></script>
|
||||||
|
% if data['media_type'] == 'library':
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
function get_history() {
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
"url": "get_history",
|
url: 'get_history',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return { 'json_data': JSON.stringify( d ),
|
return { 'json_data': JSON.stringify( d ),
|
||||||
'rating_key': ${data['rating_key']}
|
'media_type': "${data['media_type_filter']}" };
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
history_table = $('#history_table').DataTable(history_table_options);
|
|
||||||
history_table.column(4).visible(false);
|
|
||||||
|
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
|
||||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
% elif data['type'] == 'show':
|
% elif data['media_type'] == 'show' or data['media_type'] == 'artist':
|
||||||
<script src="interfaces/default/js/tables/history_table.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
function get_history() {
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
"url": "get_history",
|
url: 'get_history',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return { 'json_data': JSON.stringify( d ),
|
return { 'json_data': JSON.stringify( d ),
|
||||||
'grandparent_rating_key': ${data['rating_key']}
|
'grandparent_rating_key': "${data['rating_key']}" };
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
% elif data['media_type'] == 'season' or data['media_type'] == 'album':
|
||||||
|
<script>
|
||||||
|
function get_history() {
|
||||||
|
history_table_options.ajax = {
|
||||||
|
url: 'get_history',
|
||||||
|
type: 'post',
|
||||||
|
data: function ( d ) {
|
||||||
|
return { 'json_data': JSON.stringify( d ),
|
||||||
|
'parent_rating_key': "${data['rating_key']}" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
% elif data['media_type'] == 'episode' or data['media_type'] == 'track' or data['media_type'] == 'movie':
|
||||||
|
<script>
|
||||||
|
function get_history() {
|
||||||
|
history_table_options.ajax = {
|
||||||
|
url: 'get_history',
|
||||||
|
type: 'post',
|
||||||
|
data: function ( d ) {
|
||||||
|
return { 'json_data': JSON.stringify( d ),
|
||||||
|
'rating_key': "${data['rating_key']}" };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
history_table = $('#history_table').DataTable(history_table_options);
|
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
|
||||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
get_history();
|
||||||
|
history_table = $('#history_table').DataTable(history_table_options);
|
||||||
|
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||||
|
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
% if data['type'] == 'season':
|
clearSearchButton('history_table', history_table);
|
||||||
|
|
||||||
|
$('#row-edit-mode').on('click', function() {
|
||||||
|
$('#row-edit-mode-alert').fadeIn(200);
|
||||||
|
|
||||||
|
if ($(this).hasClass('active')) {
|
||||||
|
if (history_to_delete.length > 0) {
|
||||||
|
$('#deleteCount').text(history_to_delete.length);
|
||||||
|
$('#confirm-modal').modal();
|
||||||
|
$('#confirm-modal').one('click', '#confirm-delete', function () {
|
||||||
|
for (var i = 0; i < history_to_delete.length; i++) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_history_rows',
|
||||||
|
data: { row_id: history_to_delete[i] },
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "History deleted";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.delete-control').each(function () {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
$('#row-edit-mode-alert').fadeOut(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
history_to_delete = [];
|
||||||
|
$('.delete-control').each(function() {
|
||||||
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% if data['media_type'] == 'show' or data['media_type'] == 'season' or data['media_type'] == 'artist' or data['media_type'] == 'album':
|
||||||
<script>
|
<script>
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_children',
|
url: 'get_item_children',
|
||||||
type: "GET",
|
type: 'GET',
|
||||||
async: true,
|
async: true,
|
||||||
data: { rating_key : ${data['rating_key']} },
|
data: { rating_key : ${data['rating_key']} },
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#episode-list").html(xhr.responseText);
|
$("#children-list").html(xhr.responseText); }
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
|
% if data['media_type'] != 'library':
|
||||||
|
<!--
|
||||||
<script>
|
<script>
|
||||||
$("#runtime").html(millisecondsToMinutes($("#runtime").html(), true));
|
$('#row-edit-mode').after('<a href="info?item_id=${data['rating_key']}" class="btn btn-danger btn-edit" id="fix-metadata" \
|
||||||
|
data-toggle="tooltip" data-placement="left" title="Fix metadata if the item was moved in Plex"><i class="fa fa-wrench"></i> Fix Metadata</a>');
|
||||||
|
$('#fix-metadata').tooltip();
|
||||||
|
</script>
|
||||||
|
-->
|
||||||
|
% endif
|
||||||
|
% if data['media_type'] != 'library' and data['rating']:
|
||||||
|
<script>
|
||||||
|
// Convert rating to 5 star rating type
|
||||||
|
var starRating = Math.round(${data['rating']} / 2);
|
||||||
|
$('#stars').attr('data-rateit-value', starRating);
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
<script>
|
||||||
|
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
||||||
|
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
||||||
|
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||||
|
</script>
|
||||||
|
% elif query:
|
||||||
|
<script>
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_search_results_children',
|
||||||
|
type: 'GET',
|
||||||
|
async: true,
|
||||||
|
data: {'query': "${query['query_string'].replace('"','\\"')}",
|
||||||
|
'media_type': "${query['media_type']}",
|
||||||
|
'season_index': "${query['parent_media_index']}"
|
||||||
|
},
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#search-results-list").html(xhr.responseText); }
|
||||||
|
});
|
||||||
|
$(document).on('click', '#search-results-list a', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var new_rating_key = $(this).attr("id");
|
||||||
|
var new_href = $(this).attr("href");
|
||||||
|
|
||||||
|
$('#new_title').html($(this).find('.item-children-instance-text-wrapper').html());
|
||||||
|
|
||||||
|
$('#confirm-modal').modal();
|
||||||
|
$('#confirm-modal').one('click', '#confirm-update', function () {
|
||||||
|
$(this).prop('disabled', true);
|
||||||
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Updating database..."
|
||||||
|
showMsg(msg, false, false, 0)
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'update_history_rating_key',
|
||||||
|
data: { old_rating_key: "${query['rating_key']}",
|
||||||
|
new_rating_key: new_rating_key,
|
||||||
|
media_type: "${query['media_type']}"
|
||||||
|
},
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
window.location.href = new_href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
102
data/interfaces/default/info_children_list.html
Normal file
102
data/interfaces/default/info_children_list.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<%doc>
|
||||||
|
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
||||||
|
|
||||||
|
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
||||||
|
|
||||||
|
Filename: info_children_list.html
|
||||||
|
Version: 0.1
|
||||||
|
Variable names: data [list]
|
||||||
|
|
||||||
|
data :: Usable parameters
|
||||||
|
|
||||||
|
== Global keys ==
|
||||||
|
children_type Returns the type of children in the array.
|
||||||
|
children_count Returns the number of episodes in the array.
|
||||||
|
children_list Returns an array of episodes.
|
||||||
|
|
||||||
|
data['children_list'] :: Usable paramaters
|
||||||
|
|
||||||
|
== Global keys ==
|
||||||
|
rating_key Returns the unique identifier for the media item.
|
||||||
|
index Returns the episode number.
|
||||||
|
title Returns the name of the episode.
|
||||||
|
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
|
parent_thumb Returns the location of the item's parent thumbnail. Use with pms_image_proxy.
|
||||||
|
|
||||||
|
DOCUMENTATION :: END
|
||||||
|
</%doc>
|
||||||
|
|
||||||
|
% if data != None:
|
||||||
|
% if data['children_count'] > 0:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['children_list']:
|
||||||
|
% if child['rating_key']:
|
||||||
|
% if data['children_type'] == 'track':
|
||||||
|
<li class="item-children-list-item">
|
||||||
|
% else:
|
||||||
|
<li>
|
||||||
|
% endif
|
||||||
|
<a href="info?item_id=${child['rating_key']}">
|
||||||
|
%if data['children_type'] == 'season':
|
||||||
|
<div class="item-children-poster">
|
||||||
|
% if child['thumb']:
|
||||||
|
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);">
|
||||||
|
% else:
|
||||||
|
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450);">
|
||||||
|
% endif
|
||||||
|
<div class="item-children-card-overlay">
|
||||||
|
<div class="item-children-overlay-text">
|
||||||
|
Season ${child['index']}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif data['children_type'] == 'episode':
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);">
|
||||||
|
<div class="item-children-card-overlay">
|
||||||
|
<div class="item-children-overlay-text">
|
||||||
|
Episode ${child['index']}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper episode-item">
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
</div>
|
||||||
|
% elif data['children_type'] == 'album':
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper album-item">
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
</div>
|
||||||
|
% elif data['children_type'] == 'track':
|
||||||
|
% if loop.index % 2 == 0:
|
||||||
|
<div class="item-children-list-item-even">
|
||||||
|
<span class="item-children-list-item-index">${child['index']}</span>
|
||||||
|
<span class="item-children-list-item-title">${child['title']}</span>
|
||||||
|
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||||
|
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="item-children-list-item-odd">
|
||||||
|
<span class="item-children-list-item-index">${child['index']}</span>
|
||||||
|
<span class="item-children-list-item-title">${child['title']}</span>
|
||||||
|
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
|
||||||
|
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<%doc>
|
|
||||||
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
|
||||||
|
|
||||||
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
|
||||||
|
|
||||||
Filename: info_episode_list.html
|
|
||||||
Version: 0.1
|
|
||||||
Variable names: data [list]
|
|
||||||
|
|
||||||
data :: Usable parameters
|
|
||||||
|
|
||||||
== Global keys ==
|
|
||||||
episode_count Returns the number of episodes in the array.
|
|
||||||
episode_list Returns an array of episodes.
|
|
||||||
|
|
||||||
data['episode_list'] :: Usable paramaters
|
|
||||||
|
|
||||||
== Global keys ==
|
|
||||||
rating_key Returns the unique identifier for the media item.
|
|
||||||
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
|
||||||
title Returns the name of the episode.
|
|
||||||
index Returns the episode number.
|
|
||||||
|
|
||||||
DOCUMENTATION :: END
|
|
||||||
</%doc>
|
|
||||||
|
|
||||||
% if data != None:
|
|
||||||
% if data['episode_count'] > 0:
|
|
||||||
<div class="season-episodes-wrapper">
|
|
||||||
<ul class="season-episodes-instance list-unstyled">
|
|
||||||
% for a in data['episode_list']:
|
|
||||||
<li>
|
|
||||||
<div class="season-episodes-poster">
|
|
||||||
<div class="season-episodes-poster-face">
|
|
||||||
<a href="info?item_id=${a['rating_key']}">
|
|
||||||
<img src="pms_image_proxy?img=${a['thumb']}&width=410&height=230" class="season-episodes-poster-face">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="season-episodes-card-overlay">
|
|
||||||
<div class="season-episodes-season">
|
|
||||||
Episode ${a['index']}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="season-episodes-instance-text-wrapper">
|
|
||||||
<div class="season-episodes-title">
|
|
||||||
<a href="info?item_id=${a['rating_key']}">
|
|
||||||
"${a['title']}"
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
% endfor
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
% endif
|
|
||||||
% endif
|
|
||||||
|
|
||||||
224
data/interfaces/default/info_search_results_list.html
Normal file
224
data/interfaces/default/info_search_results_list.html
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<%doc>
|
||||||
|
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
||||||
|
|
||||||
|
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
||||||
|
|
||||||
|
Filename: info_children_list.html
|
||||||
|
Version: 0.1
|
||||||
|
Variable names: data [list]
|
||||||
|
|
||||||
|
data :: Usable parameters
|
||||||
|
|
||||||
|
== Global keys ==
|
||||||
|
results_count Returns the number of search results.
|
||||||
|
results_list Returns a dictionary of search result types.
|
||||||
|
|
||||||
|
data['results_list'] :: Usable paramaters
|
||||||
|
|
||||||
|
== media_type keys ==
|
||||||
|
movie Returns an array of movie results
|
||||||
|
show Returns an array of show results
|
||||||
|
season Returns an array of season results
|
||||||
|
episode Returns an array of episode results
|
||||||
|
artist Returns an array of artist results
|
||||||
|
album Returns an array of album results
|
||||||
|
track Returns an array of track results
|
||||||
|
|
||||||
|
data['results_list'][media_type] :: Usable paramaters
|
||||||
|
|
||||||
|
== Global keys ==
|
||||||
|
rating_key Returns the unique identifier for the media item.
|
||||||
|
type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'.
|
||||||
|
art Returns the location of the item's artwork
|
||||||
|
title Returns the name of the movie, show, episode, artist, album, or track.
|
||||||
|
duration Returns the standard runtime of the media.
|
||||||
|
content_rating Returns the age rating for the media.
|
||||||
|
summary Returns a brief description of the media plot.
|
||||||
|
grandparent_title Returns the name of the show, or artist.
|
||||||
|
parent_index Returns the index number of the season.
|
||||||
|
index Returns the index number of the episode, or track.
|
||||||
|
parent_thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
|
writers Returns an array of writers.
|
||||||
|
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
|
parent_title Returns the name of the show, or artist.
|
||||||
|
rating Returns the 5 star rating value for the movie. Between 1 and 5.
|
||||||
|
year Returns the release year of the movie, or show.
|
||||||
|
genres Returns an array of genres.
|
||||||
|
actors Returns an array of actors.
|
||||||
|
directors Returns an array of directors.
|
||||||
|
studio Returns the name of the studio.
|
||||||
|
originally_available_at Returns the air date of the item.
|
||||||
|
|
||||||
|
DOCUMENTATION :: END
|
||||||
|
</%doc>
|
||||||
|
|
||||||
|
% if data != None:
|
||||||
|
% if data['results_count'] > 0:
|
||||||
|
% if 'movie' in data['results_list'] and data['results_list']['movie']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>Movies</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['movie']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper season-item">
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
<h3 class="text-muted">${child['year']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if 'show' in data['results_list'] and data['results_list']['show']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>TV Shows</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['show']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper season-item">
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
<h3 class="text-muted">${child['year']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if 'season' in data['results_list'] and data['results_list']['season']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>Seasons</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['season']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper season-item">
|
||||||
|
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
|
||||||
|
<h3 class="text-muted">S${child['index']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if 'episode' in data['results_list'] and data['results_list']['episode']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>Episodes</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['episode']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper episode-item">
|
||||||
|
<h3 title="${child['grandparent_title']}">${child['grandparent_title']}</h3>
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
<h3 class="text-muted">S${child['parent_index']} · E${child['index']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if 'artist' in data['results_list'] and data['results_list']['artist']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>Artists</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['artist']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper album-item">
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if 'album' in data['results_list'] and data['results_list']['album']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>Albums</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['album']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper album-item">
|
||||||
|
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if 'track' in data['results_list'] and data['results_list']['track']:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
<div class="item-children-section-title">
|
||||||
|
<h4>Tracks</h4>
|
||||||
|
</div>
|
||||||
|
<ul class="item-children-instance list-unstyled">
|
||||||
|
% for child in data['results_list']['track']:
|
||||||
|
<li>
|
||||||
|
<a href="info?item_id=${child['rating_key']}" id="${child['rating_key']}">
|
||||||
|
<div class="item-children-poster">
|
||||||
|
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300);">
|
||||||
|
<div class="item-children-card-overlay">
|
||||||
|
<div class="item-children-overlay-text">
|
||||||
|
Track ${child['index']}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-children-instance-text-wrapper album-item">
|
||||||
|
<h3 title="${child['grandparent_title']}">${child['grandparent_title']}</h3>
|
||||||
|
<h3 title="${child['title']}">${child['title']}</h3>
|
||||||
|
<h3 title="${child['parent_title']}" class="text-muted">${child['parent_title']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% else:
|
||||||
|
<div class="item-children-wrapper">
|
||||||
|
No search results found.
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
<div class="modal-body" id="modal-text">
|
<div class="modal-body" id="modal-text">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><strong>Location Details</strong></h4>
|
<h4><strong>Location Details</strong></h4>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
<li>Country: <strong><span id="country"></span></strong></li>
|
<li>Country: <strong><span id="country"></span></strong></li>
|
||||||
<li>City: <strong><span id="city"></span></strong></li>
|
|
||||||
<li>Region: <strong><span id="region"></span></strong></li>
|
<li>Region: <strong><span id="region"></span></strong></li>
|
||||||
|
<li>City: <strong><span id="city"></span></strong></li>
|
||||||
<li>Timezone: <strong><span id="timezone"></span></strong></li>
|
<li>Timezone: <strong><span id="timezone"></span></strong></li>
|
||||||
<li>Latitude: <strong><span id="lat"></span></strong></li>
|
<li>Latitude: <strong><span id="lat"></span></strong></li>
|
||||||
<li>Longitude: <strong><span id="lon"></span></strong></li>
|
<li>Longitude: <strong><span id="lon"></span></strong></li>
|
||||||
@@ -26,15 +26,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h4><strong>Connection Details</strong></h4>
|
<h4><strong>Connection Details</strong></h4>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
<li>ISP: <strong><span id="isp"></span></strong></li>
|
<li>Organization: <strong><span id="organization"></span></strong></li>
|
||||||
<li>Organization: <strong><span id="org"></span></strong></li>
|
|
||||||
<li>AS: <strong><span id="as"></span></strong></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<span class="text-muted">Service provided by ip-api.com.</span>
|
<span class="text-muted">Telize service written by <a href="https://github.com/fcambus/telize" target="_blank">Frederic Cambus</a>.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +41,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function getUserLocation(ip_address) {
|
function getUserLocation(ip_address) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'http://ip-api.com/json/' + ip_address,
|
url: 'https://telize.myhtpc.co.za/geoip/' + ip_address,
|
||||||
cache: true,
|
cache: true,
|
||||||
async: true,
|
async: true,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
@@ -55,13 +53,11 @@
|
|||||||
$('#modal_header_ip_address').html('<i class="fa fa-map-marker"></i> IP Address: ' + ip_address);
|
$('#modal_header_ip_address').html('<i class="fa fa-map-marker"></i> IP Address: ' + ip_address);
|
||||||
$('#country').html(data.country);
|
$('#country').html(data.country);
|
||||||
$('#city').html(data.city);
|
$('#city').html(data.city);
|
||||||
$('#region').html(data.regionName);
|
$('#region').html(data.region);
|
||||||
$('#timezone').html(data.timezone);
|
$('#timezone').html(data.timezone);
|
||||||
$('#lat').html(data.lat);
|
$('#lat').html(data.latitude);
|
||||||
$('#lon').html(data.lon);
|
$('#lon').html(data.longitude);
|
||||||
$('#isp').html(data.isp);
|
$('#organization').html(data.organization);
|
||||||
$('#org').html(data.org);
|
|
||||||
$('#as').html(data.as);
|
|
||||||
},
|
},
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ var hc_plays_by_day_options = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
labels: {
|
labels: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var hc_plays_by_dayofweek_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [{}],
|
categories: [{}],
|
||||||
labels: {
|
labels: {
|
||||||
@@ -46,6 +46,24 @@ var hc_plays_by_dayofweek_options = {
|
|||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
stackLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
column: {
|
||||||
|
stacking: 'normal',
|
||||||
|
borderWidth: '0',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#000'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var hc_plays_by_hourofday_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [{}],
|
categories: [{}],
|
||||||
labels: {
|
labels: {
|
||||||
@@ -46,6 +46,24 @@ var hc_plays_by_hourofday_options = {
|
|||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
stackLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
column: {
|
||||||
|
stacking: 'normal',
|
||||||
|
borderWidth: '0',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#000'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|||||||
@@ -23,13 +23,9 @@ var hc_plays_by_month_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'datetime',
|
|
||||||
labels: {
|
labels: {
|
||||||
formatter: function() {
|
|
||||||
return moment(this.value).format("MMM YYYY");
|
|
||||||
},
|
|
||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var hc_plays_by_platform_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [{}],
|
categories: [{}],
|
||||||
labels: {
|
labels: {
|
||||||
@@ -46,6 +46,24 @@ var hc_plays_by_platform_options = {
|
|||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
stackLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
column: {
|
||||||
|
stacking: 'normal',
|
||||||
|
borderWidth: '0',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#000'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var hc_plays_by_source_resolution_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [{}],
|
categories: [{}],
|
||||||
labels: {
|
labels: {
|
||||||
@@ -46,6 +46,24 @@ var hc_plays_by_source_resolution_options = {
|
|||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
stackLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
column: {
|
||||||
|
stacking: 'normal',
|
||||||
|
borderWidth: '0',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#000'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var hc_plays_by_stream_resolution_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [{}],
|
categories: [{}],
|
||||||
labels: {
|
labels: {
|
||||||
@@ -46,6 +46,24 @@ var hc_plays_by_stream_resolution_options = {
|
|||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
stackLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
column: {
|
||||||
|
stacking: 'normal',
|
||||||
|
borderWidth: '0',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#000'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ var hc_plays_by_user_options = {
|
|||||||
credits: {
|
credits: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
colors: ['#F9AA03', '#FFFFFF'],
|
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
|
||||||
xAxis: {
|
xAxis: {
|
||||||
categories: [{}],
|
categories: [{}],
|
||||||
labels: {
|
labels: {
|
||||||
@@ -46,6 +46,24 @@ var hc_plays_by_user_options = {
|
|||||||
style: {
|
style: {
|
||||||
color: '#aaa'
|
color: '#aaa'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
stackLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
column: {
|
||||||
|
stacking: 'normal',
|
||||||
|
borderWidth: '0',
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
style: {
|
||||||
|
color: '#000'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|||||||
@@ -176,7 +176,9 @@ function getPlatformImagePath(platformName) {
|
|||||||
if (platformName.indexOf("Roku") > -1) {
|
if (platformName.indexOf("Roku") > -1) {
|
||||||
return 'interfaces/default/images/platforms/roku.png';
|
return 'interfaces/default/images/platforms/roku.png';
|
||||||
} else if (platformName.indexOf("Apple TV") > -1) {
|
} else if (platformName.indexOf("Apple TV") > -1) {
|
||||||
return 'interfaces/default/images/platforms/appletv.png';
|
return 'interfaces/default/images/platforms/atv.png';
|
||||||
|
} else if (platformName.indexOf("tvOS") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/atv.png';
|
||||||
} else if (platformName.indexOf("Firefox") > -1) {
|
} else if (platformName.indexOf("Firefox") > -1) {
|
||||||
return 'interfaces/default/images/platforms/firefox.png';
|
return 'interfaces/default/images/platforms/firefox.png';
|
||||||
} else if (platformName.indexOf("Chromecast") > -1) {
|
} else if (platformName.indexOf("Chromecast") > -1) {
|
||||||
@@ -201,6 +203,8 @@ function getPlatformImagePath(platformName) {
|
|||||||
return 'interfaces/default/images/platforms/safari.png';
|
return 'interfaces/default/images/platforms/safari.png';
|
||||||
} else if (platformName.indexOf("Internet Explorer") > -1) {
|
} else if (platformName.indexOf("Internet Explorer") > -1) {
|
||||||
return 'interfaces/default/images/platforms/ie.png';
|
return 'interfaces/default/images/platforms/ie.png';
|
||||||
|
} else if (platformName.indexOf("Microsoft Edge") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/msedge.png';
|
||||||
} else if (platformName.indexOf("Unknown Browser") > -1) {
|
} else if (platformName.indexOf("Unknown Browser") > -1) {
|
||||||
return 'interfaces/default/images/platforms/dafault.png';
|
return 'interfaces/default/images/platforms/dafault.png';
|
||||||
} else if (platformName.indexOf("Windows-XBMC") > -1) {
|
} else if (platformName.indexOf("Windows-XBMC") > -1) {
|
||||||
@@ -213,12 +217,18 @@ function getPlatformImagePath(platformName) {
|
|||||||
return 'interfaces/default/images/platforms/opera.png';
|
return 'interfaces/default/images/platforms/opera.png';
|
||||||
} else if (platformName.indexOf("KODI") > -1) {
|
} else if (platformName.indexOf("KODI") > -1) {
|
||||||
return 'interfaces/default/images/platforms/kodi.png';
|
return 'interfaces/default/images/platforms/kodi.png';
|
||||||
} else if (platformName.indexOf("Mystery 3") > -1) {
|
} else if (platformName.indexOf("Playstation 3") > -1) {
|
||||||
return 'interfaces/default/images/platforms/playstation.png';
|
return 'interfaces/default/images/platforms/playstation.png';
|
||||||
} else if (platformName.indexOf("Mystery 4") > -1) {
|
} else if (platformName.indexOf("Playstation 4") > -1) {
|
||||||
return 'interfaces/default/images/platforms/playstation.png';
|
return 'interfaces/default/images/platforms/playstation.png';
|
||||||
} else if (platformName.indexOf("Mystery 5") > -1) {
|
} else if (platformName.indexOf("Xbox 360") > -1) {
|
||||||
return 'interfaces/default/images/platforms/xbox.png';
|
return 'interfaces/default/images/platforms/xbox.png';
|
||||||
|
} else if (platformName.indexOf("Windows") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/win8.png';
|
||||||
|
} else if (platformName.indexOf("Windows phone") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/wp.png';
|
||||||
|
} else if (platformName.indexOf("Plex Media Player") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/pmp.png';
|
||||||
} else {
|
} else {
|
||||||
return 'interfaces/default/images/platforms/default.png';
|
return 'interfaces/default/images/platforms/default.png';
|
||||||
}
|
}
|
||||||
@@ -226,7 +236,9 @@ function getPlatformImagePath(platformName) {
|
|||||||
|
|
||||||
function isPrivateIP(ip_address) {
|
function isPrivateIP(ip_address) {
|
||||||
if (ip_address.indexOf(".") > -1) {
|
if (ip_address.indexOf(".") > -1) {
|
||||||
var parts = ip_address.split('.');
|
// get IPv4 mapped address (xxx.xxx.xxx.xxx) from IPv6 addresss (::ffff:xxx.xxx.xxx.xxx)
|
||||||
|
var parts = ip_address.split(":");
|
||||||
|
var parts = parts[parts.length - 1].split('.');
|
||||||
if (parts[0] === '10' ||
|
if (parts[0] === '10' ||
|
||||||
(parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) ||
|
(parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) ||
|
||||||
(parts[0] === '192' && parts[1] === '168')) {
|
(parts[0] === '192' && parts[1] === '168')) {
|
||||||
@@ -251,6 +263,9 @@ function humanTime(seconds) {
|
|||||||
} else if (seconds >= 60) {
|
} else if (seconds >= 60) {
|
||||||
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||||
return text;
|
return text;
|
||||||
|
} else {
|
||||||
|
text = '<h3>0</h3><p> mins</p>';
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,3 +363,12 @@ Accordion.prototype.dropdown = function(e) {
|
|||||||
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearSearchButton(tableName, table) {
|
||||||
|
$('#' + tableName + '_filter').find('input[type=search]')
|
||||||
|
.wrap('<div class="input-group" role="group" aria-label="Search"></div>')
|
||||||
|
.after('<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' + tableName + '"><i class="fa fa-remove"></i></button></span>')
|
||||||
|
$('#clear-search-' + tableName).click(function() {
|
||||||
|
table.search('').draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
3
data/interfaces/default/js/selectize.min.js
vendored
Normal file
3
data/interfaces/default/js/selectize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
var date_format = 'YYYY-MM-DD';
|
var date_format = 'YYYY-MM-DD';
|
||||||
var time_format = 'hh:mm a';
|
var time_format = 'hh:mm a';
|
||||||
|
var history_to_delete = [];
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_date_formats',
|
url: 'get_date_formats',
|
||||||
@@ -18,30 +19,48 @@ history_table_options = {
|
|||||||
"info":"Showing _START_ to _END_ of _TOTAL_ history items",
|
"info":"Showing _START_ to _END_ of _TOTAL_ history items",
|
||||||
"infoEmpty":"Showing 0 to 0 of 0 entries",
|
"infoEmpty":"Showing 0 to 0 of 0 entries",
|
||||||
"infoFiltered":"(filtered from _MAX_ total entries)",
|
"infoFiltered":"(filtered from _MAX_ total entries)",
|
||||||
"emptyTable": "No data in table",
|
"emptyTable": "No data in table"
|
||||||
},
|
},
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"stateSave": true,
|
"stateSave": true,
|
||||||
"processing": false,
|
"processing": false,
|
||||||
"serverSide": true,
|
"serverSide": true,
|
||||||
"pageLength": 25,
|
"pageLength": 25,
|
||||||
"order": [ 0, 'desc'],
|
"order": [ 1, 'desc'],
|
||||||
|
"autoWidth": false,
|
||||||
"columnDefs": [
|
"columnDefs": [
|
||||||
{
|
{
|
||||||
"targets": [0],
|
"targets": [0],
|
||||||
|
"data": null,
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
$(td).html('<button class="btn btn-xs btn-warning" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
|
||||||
|
},
|
||||||
|
"width": "5%",
|
||||||
|
"className": "delete-control no-wrap hidden",
|
||||||
|
"searchable": false,
|
||||||
|
"orderable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [1],
|
||||||
"data":"date",
|
"data":"date",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (rowData['stopped'] === null) {
|
if (rowData['stopped'] === null) {
|
||||||
$(td).html('Currently watching...');
|
$(td).html('Currently watching...');
|
||||||
|
} else if (rowData['group_count'] > 1) {
|
||||||
|
date = moment(cellData, "X").format(date_format);
|
||||||
|
expand_history = '<span class="expand-history-tooltip" data-toggle="tooltip" title="Show Detailed History"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
||||||
|
$(td).html('<div><a href="#"><div style="float: left;">' + expand_history + ' ' + date + '</div></a></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html(moment(cellData,"X").format(date_format));
|
date = moment(cellData, "X").format(date_format);
|
||||||
|
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i> ' + date + '</div>');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"className": "no-wrap"
|
"width": "8%",
|
||||||
|
"className": "no-wrap expand-history"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [1],
|
"targets": [2],
|
||||||
"data":"friendly_name",
|
"data":"friendly_name",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== '') {
|
if (cellData !== '') {
|
||||||
@@ -54,21 +73,12 @@ history_table_options = {
|
|||||||
$(td).html(cellData);
|
$(td).html(cellData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"width": "8%",
|
||||||
"className": "no-wrap hidden-xs"
|
"className": "no-wrap hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"targets": [2],
|
|
||||||
"data":"player",
|
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
|
||||||
if (cellData !== '') {
|
|
||||||
$(td).html('<a href="#" data-target="#info-modal" data-toggle="modal"><i class="fa fa-lg fa-info-circle"></i></a> '+cellData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"className": "modal-control no-wrap hidden-sm hidden-xs"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"targets": [3],
|
"targets": [3],
|
||||||
"data":"ip_address",
|
"data": "ip_address",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData) {
|
if (cellData) {
|
||||||
if (isPrivateIP(cellData)) {
|
if (isPrivateIP(cellData)) {
|
||||||
@@ -78,35 +88,75 @@ history_table_options = {
|
|||||||
$(td).html('n/a');
|
$(td).html('n/a');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><i class="fa fa-map-marker"></i> ' + cellData +'</a>');
|
external_ip = '<span class="external-ip-tooltip" data-toggle="tooltip" title="External IP"><i class="fa fa-map-marker fa-fw"></i></span>';
|
||||||
|
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal">'+ external_ip + cellData + '</a>');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$(td).html('n/a');
|
$(td).html('n/a');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"className": "no-wrap hidden-xs modal-control-ip"
|
"width": "8%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4],
|
"targets": [4],
|
||||||
|
"data":"platform",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "8%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [5],
|
||||||
|
"data": "player",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
var transcode_dec = '';
|
||||||
|
if (rowData['video_decision'] === 'transcode' || rowData['audio_decision'] === 'transcode') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||||
|
} else if (rowData['video_decision'] === 'copy' || rowData['audio_decision'] === 'copy') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||||
|
} else if (rowData['video_decision'] === 'direct play' || rowData['audio_decision'] === 'direct play') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||||
|
}
|
||||||
|
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "12%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [6],
|
||||||
"data":"full_title",
|
"data":"full_title",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== '') {
|
if (cellData !== '') {
|
||||||
if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') {
|
var media_type = '';
|
||||||
var transcode_dec = '';
|
var thumb_popover = '';
|
||||||
if (rowData['video_decision'] === 'transcode') {
|
if (rowData['media_type'] === 'movie') {
|
||||||
transcode_dec = '<i class="fa fa-server"></i> ';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||||
}
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' (' + rowData['year'] + ')</span>'
|
||||||
$(td).html('<div><div style="float: left;"><a href="info?source=history&item_id=' + rowData['id'] + '">' + cellData + '</a></div><div style="float: right; text-align: right; padding-right: 5px;">' + transcode_dec + '<i class="fa fa-video-camera"></i></div></div>');
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'episode') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' \
|
||||||
|
(S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'track') {
|
} else if (rowData['media_type'] === 'track') {
|
||||||
$(td).html('<div><div style="float: left;">' + cellData + '</div><div style="float: right; text-align: right; padding-right: 5px;"><i class="fa fa-music"></i></div></div>');
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"width": "35%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [5],
|
"targets": [7],
|
||||||
"data":"started",
|
"data":"started",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData === null) {
|
if (cellData === null) {
|
||||||
@@ -116,12 +166,13 @@ history_table_options = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
|
"width": "5%",
|
||||||
"className": "no-wrap hidden-sm hidden-xs"
|
"className": "no-wrap hidden-sm hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [6],
|
"targets": [8],
|
||||||
"data":"paused_counter",
|
"data":"paused_counter",
|
||||||
"render": function ( data, type, full ) {
|
"render": function (data, type, full) {
|
||||||
if (data !== null) {
|
if (data !== null) {
|
||||||
return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins';
|
return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins';
|
||||||
} else {
|
} else {
|
||||||
@@ -129,10 +180,11 @@ history_table_options = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"className": "no-wrap hidden-xs"
|
"width": "5%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [7],
|
"targets": [9],
|
||||||
"data":"stopped",
|
"data":"stopped",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData === null) {
|
if (cellData === null) {
|
||||||
@@ -142,12 +194,13 @@ history_table_options = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"className": "no-wrap hidden-md hidden-xs"
|
"width": "5%",
|
||||||
|
"className": "no-wrap hidden-sm hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [8],
|
"targets": [10],
|
||||||
"data":"duration",
|
"data":"duration",
|
||||||
"render": function ( data, type, full ) {
|
"render": function (data, type, full) {
|
||||||
if (data !== null) {
|
if (data !== null) {
|
||||||
return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins';
|
return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins';
|
||||||
} else {
|
} else {
|
||||||
@@ -155,43 +208,100 @@ history_table_options = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
|
"width": "5%",
|
||||||
"className": "no-wrap hidden-xs"
|
"className": "no-wrap hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [9],
|
"targets": [11],
|
||||||
"data":"percent_complete",
|
"data": "watched_status",
|
||||||
"render": function ( data, type, full ) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (data > 80) {
|
if (cellData == 1) {
|
||||||
return '<i class="fa fa-lg fa-circle"></i>'
|
$(td).html('<span class="watched-tooltip" data-toggle="tooltip" title="' + rowData['percent_complete'] + '%"><i class="fa fa-lg fa-circle"></i></span>');
|
||||||
} else if (data > 40) {
|
} else if (cellData == 0.5) {
|
||||||
return '<i class="fa fa-lg fa-adjust fa-rotate-180"></i>'
|
$(td).html('<span class="watched-tooltip" data-toggle="tooltip" title="' + rowData['percent_complete'] + '%"><i class="fa fa-lg fa-adjust fa-rotate-180"></i></span>');
|
||||||
} else {
|
} else {
|
||||||
return '<i class="fa fa-lg fa-circle-o"></i>'
|
$(td).html('<span class="watched-tooltip" data-toggle="tooltip" title="' + rowData['percent_complete'] + '%"><i class="fa fa-lg fa-circle-o"></i></span>');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"orderable": false,
|
"orderable": false,
|
||||||
"className": "no-wrap hidden-md hidden-xs",
|
"className": "no-wrap hidden-md hidden-sm hidden-xs",
|
||||||
"width": "10px"
|
"width": "1%"
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"drawCallback": function (settings) {
|
"drawCallback": function (settings) {
|
||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
// $('html,body').scrollTop(0);
|
// $('html,body').scrollTop(0);
|
||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
// Create the tooltips.
|
// Create the tooltips.
|
||||||
$('.info-modal').each(function() {
|
$('.expand-history-tooltip').tooltip({ container: 'body' });
|
||||||
$(this).tooltip();
|
$('.external-ip-tooltip').tooltip({ container: 'body' });
|
||||||
|
$('.transcode-tooltip').tooltip({ container: 'body' });
|
||||||
|
$('.media-type-tooltip').tooltip({ container: 'body' });
|
||||||
|
$('.watched-tooltip').tooltip({ container: 'body' });
|
||||||
|
$('.thumb-tooltip').popover({
|
||||||
|
html: true,
|
||||||
|
container: 'body',
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'right',
|
||||||
|
content: function () {
|
||||||
|
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px;" />';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($('#row-edit-mode').hasClass('active')) {
|
||||||
|
$('.delete-control').each(function () {
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
history_table.rows().every(function () {
|
||||||
|
var rowData = this.data();
|
||||||
|
if (rowData['group_count'] != 1 && rowData['reference_id'] in history_child_table) {
|
||||||
|
// if grouped row and a child table was already created
|
||||||
|
$(this.node()).find('i.fa').toggleClass('fa-plus-circle').toggleClass('fa-minus-circle');
|
||||||
|
this.child(childTableFormat(rowData)).show();
|
||||||
|
createChildTable(this, rowData)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
"preDrawCallback": function(settings) {
|
"preDrawCallback": function(settings) {
|
||||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
|
},
|
||||||
|
"rowCallback": function (row, rowData, rowIndex) {
|
||||||
|
if (rowData['group_count'] == 1) {
|
||||||
|
// if no grouped rows simply toggle the delete button
|
||||||
|
if ($.inArray(rowData['id'], history_to_delete) !== -1) {
|
||||||
|
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if grouped rows
|
||||||
|
// toggle the parent button to danger
|
||||||
|
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
// check if any child rows are not selected
|
||||||
|
var group_ids = rowData['group_ids'].split(',').map(Number);
|
||||||
|
group_ids.forEach(function (id) {
|
||||||
|
var index = $.inArray(id, history_to_delete);
|
||||||
|
if (index == -1) {
|
||||||
|
$(row).find('button[data-id="' + rowData['id'] + '"]').addClass('btn-warning').removeClass('btn-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowData['group_count'] != 1 && rowData['reference_id'] in history_child_table) {
|
||||||
|
// if grouped row and a child table was already created
|
||||||
|
$(row).addClass('shown')
|
||||||
|
history_table.row(row).child(childTableFormat(rowData)).show();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#history_table').on('click', 'td.modal-control', function () {
|
// Parent table platform modal
|
||||||
var tr = $(this).parents('tr');
|
$('#history_table').on('click', '> tbody > tr > td.modal-control', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
var row = history_table.row( tr );
|
var row = history_table.row( tr );
|
||||||
var rowData = row.data();
|
var rowData = row.data();
|
||||||
|
|
||||||
@@ -209,8 +319,9 @@ $('#history_table').on('click', 'td.modal-control', function () {
|
|||||||
showStreamDetails();
|
showStreamDetails();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#history_table').on('click', 'td.modal-control-ip', function () {
|
// Parent table ip address modal
|
||||||
var tr = $(this).parents('tr');
|
$('#history_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
var row = history_table.row( tr );
|
var row = history_table.row( tr );
|
||||||
var rowData = row.data();
|
var rowData = row.data();
|
||||||
|
|
||||||
@@ -228,6 +339,242 @@ $('#history_table').on('click', 'td.modal-control-ip', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserLocation(rowData['ip_address']);
|
getUserLocation(rowData['ip_address']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Parent table delete mode
|
||||||
|
$('#history_table').on('click', '> tbody > tr > td.delete-control > button', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
|
var row = history_table.row( tr );
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
if (rowData['group_count'] == 1) {
|
||||||
|
// if no grouped rows simply add or remove row from history_to_delete
|
||||||
|
var index = $.inArray(rowData['id'], history_to_delete);
|
||||||
|
if (index === -1) {
|
||||||
|
history_to_delete.push(rowData['id']);
|
||||||
|
} else {
|
||||||
|
history_to_delete.splice(index, 1);
|
||||||
|
}
|
||||||
|
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
} else {
|
||||||
|
// if grouped rows
|
||||||
|
if ($(this).hasClass('btn-warning')) {
|
||||||
|
// add all grouped rows to history_to_delete
|
||||||
|
var group_ids = rowData['group_ids'].split(',').map(Number);
|
||||||
|
group_ids.forEach(function (id) {
|
||||||
|
var index = $.inArray(id, history_to_delete);
|
||||||
|
if (index == -1) {
|
||||||
|
history_to_delete.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
if (row.child.isShown()) {
|
||||||
|
// if child table is visible, toggle all child buttons to danger
|
||||||
|
tr.next().find('td.delete-control > button.btn-warning').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// remove all grouped rows to history_to_delete
|
||||||
|
var group_ids = rowData['group_ids'].split(',').map(Number);
|
||||||
|
group_ids.forEach(function (id) {
|
||||||
|
var index = $.inArray(id, history_to_delete);
|
||||||
|
if (index != -1) {
|
||||||
|
history_to_delete.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
if (row.child.isShown()) {
|
||||||
|
// if child table is visible, toggle all child buttons to warning
|
||||||
|
tr.next().find('td.delete-control > button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parent table expand detailed history
|
||||||
|
$('#history_table').on('click', '> tbody > tr > td.expand-history a', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
|
var row = history_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
$(this).find('i.fa').toggleClass('fa-plus-circle').toggleClass('fa-minus-circle');
|
||||||
|
|
||||||
|
if (row.child.isShown()) {
|
||||||
|
$('div.slider', row.child()).slideUp(function () {
|
||||||
|
row.child.hide();
|
||||||
|
tr.removeClass('shown');
|
||||||
|
delete history_child_table[rowData['reference_id']];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tr.addClass('shown');
|
||||||
|
row.child(childTableFormat(rowData)).show();
|
||||||
|
createChildTable(row, rowData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize the detailed history child table options using the parent table options
|
||||||
|
function childTableOptions(rowData) {
|
||||||
|
history_child_options = history_table_options;
|
||||||
|
// Remove settings that are not necessary
|
||||||
|
history_child_options.searching = false;
|
||||||
|
history_child_options.lengthChange = false;
|
||||||
|
history_child_options.info = false;
|
||||||
|
history_child_options.pageLength = 10;
|
||||||
|
history_child_options.bStateSave = false;
|
||||||
|
history_child_options.ajax = {
|
||||||
|
"url": "get_history",
|
||||||
|
type: "post",
|
||||||
|
data: function (d) {
|
||||||
|
return {
|
||||||
|
'json_data': JSON.stringify(d),
|
||||||
|
'grouping': false,
|
||||||
|
'reference_id': rowData['reference_id']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
history_child_options.fnDrawCallback = function (settings) {
|
||||||
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
|
// Create the tooltips.
|
||||||
|
$('.expand-history-tooltip').tooltip({ container: 'body' });
|
||||||
|
$('.external-ip-tooltip').tooltip();
|
||||||
|
$('.transcode-tooltip').tooltip();
|
||||||
|
$('.media-type-tooltip').tooltip();
|
||||||
|
$('.watched-tooltip').tooltip();
|
||||||
|
$('.thumb-tooltip').popover({
|
||||||
|
html: true,
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'right',
|
||||||
|
content: function () {
|
||||||
|
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px;" />';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($('#row-edit-mode').hasClass('active')) {
|
||||||
|
$('.delete-control').each(function () {
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).closest('div.slider').slideDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
return history_child_options;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the detailed history child table
|
||||||
|
function childTableFormat(rowData) {
|
||||||
|
return '<div class="slider">' +
|
||||||
|
'<table id="history_child-' + rowData['reference_id'] + '">' +
|
||||||
|
'<thead>' +
|
||||||
|
'<tr>' +
|
||||||
|
'<th align="left" id="delete_row">Delete</th>' +
|
||||||
|
'<th align="left" id="time">Time</th>' +
|
||||||
|
'<th align="left" id="friendly_name">User</th>' +
|
||||||
|
'<th align="left" id="ip_address">IP Address</th>' +
|
||||||
|
'<th align="left" id="platform">Platform</th>' +
|
||||||
|
'<th align="left" id="platform">Player</th>' +
|
||||||
|
'<th align="left" id="title">Title</th>' +
|
||||||
|
'<th align="left" id="started">Started</th>' +
|
||||||
|
'<th align="left" id="paused_counter">Paused</th>' +
|
||||||
|
'<th align="left" id="stopped">Stopped</th>' +
|
||||||
|
'<th align="left" id="duration">Duration</th>' +
|
||||||
|
'<th align="left" id="percent_complete"></th>' +
|
||||||
|
'</tr>' +
|
||||||
|
'</thead>' +
|
||||||
|
'<tbody>' +
|
||||||
|
'</tbody>' +
|
||||||
|
'</table>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the detailed history child table
|
||||||
|
history_child_table = {};
|
||||||
|
function createChildTable(row, rowData) {
|
||||||
|
history_child_options = childTableOptions(rowData);
|
||||||
|
// initialize the child table
|
||||||
|
history_child_table[rowData['reference_id']] = $('#history_child-' + rowData['reference_id']).DataTable(history_child_options);
|
||||||
|
|
||||||
|
// Set child table column visibility to match parent table
|
||||||
|
var visibility = history_table.columns().visible();
|
||||||
|
for (var i = 0; i < visibility.length; i++) {
|
||||||
|
if (!(visibility[i])) { history_child_table[rowData['reference_id']].column(i).visible(visibility[i]); }
|
||||||
|
}
|
||||||
|
history_table.on('column-visibility', function (e, settings, colIdx, visibility) {
|
||||||
|
if (row.child.isShown()) {
|
||||||
|
history_child_table[rowData['reference_id']].column(colIdx).visible(visibility);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Child table platform modal
|
||||||
|
$('#history_child-' + rowData['reference_id']).on('click', 'td.modal-control', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
|
var childRow = history_child_table[rowData['reference_id']].row(tr);
|
||||||
|
var childRowData = childRow.data();
|
||||||
|
|
||||||
|
function showStreamDetails() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_stream_data',
|
||||||
|
data: { row_id: childRowData['id'], user: childRowData['friendly_name'] },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#info-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showStreamDetails();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Child table ip address modal
|
||||||
|
$('#history_child-' + rowData['reference_id']).on('click', 'td.modal-control-ip', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
|
var childRow = history_child_table[rowData['reference_id']].row(tr);
|
||||||
|
var childRowData = childRow.data();
|
||||||
|
|
||||||
|
function getUserLocation(ip_address) {
|
||||||
|
if (isPrivateIP(ip_address)) {
|
||||||
|
return "n/a"
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_ip_address_details',
|
||||||
|
data: { ip_address: ip_address },
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#ip-info-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getUserLocation(childRowData['ip_address']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Child table delete mode
|
||||||
|
$('#history_child-' + rowData['reference_id']).on('click', 'td.delete-control > button', function () {
|
||||||
|
var tr = $(this).closest('tr');
|
||||||
|
var childRow = history_child_table[rowData['reference_id']].row(tr);
|
||||||
|
var childRowData = childRow.data();
|
||||||
|
|
||||||
|
// add or remove row from history_to_delete
|
||||||
|
var index = $.inArray(childRowData['id'], history_to_delete);
|
||||||
|
if (index === -1) {
|
||||||
|
history_to_delete.push(childRowData['id']);
|
||||||
|
} else {
|
||||||
|
history_to_delete.splice(index, 1);
|
||||||
|
}
|
||||||
|
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
|
||||||
|
tr.parents('tr').prev().find('td.delete-control > button.btn-warning').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
// check if any child rows are not selected
|
||||||
|
var group_ids = rowData['group_ids'].split(',').map(Number);
|
||||||
|
group_ids.forEach(function (id) {
|
||||||
|
var index = $.inArray(id, history_to_delete);
|
||||||
|
if (index == -1) {
|
||||||
|
// if any child row is not selected, toggle parent button to warning
|
||||||
|
tr.parents('tr').prev().find('td.delete-control > button.btn-danger').addClass('btn-warning').removeClass('btn-danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,21 +74,41 @@ history_table_modal_options = {
|
|||||||
{
|
{
|
||||||
"targets": [3],
|
"targets": [3],
|
||||||
"data":"player",
|
"data":"player",
|
||||||
"className": "modal-control no-wrap hidden-sm hidden-xs"
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
var transcode_dec = '';
|
||||||
|
if (rowData['video_decision'] === 'transcode' || rowData['audio_decision'] === 'transcode') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||||
|
} else if (rowData['video_decision'] === 'copy' || rowData['audio_decision'] === 'copy') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||||
|
} else if (rowData['video_decision'] === 'direct play' || rowData['audio_decision'] === 'direct play') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||||
|
}
|
||||||
|
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"className": "no-wrap hidden-sm hidden-xs modal-control"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4],
|
"targets": [4],
|
||||||
"data":"full_title",
|
"data":"full_title",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== '') {
|
if (cellData !== '') {
|
||||||
if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') {
|
var media_type = '';
|
||||||
var transcode_dec = '';
|
var thumb_popover = '';
|
||||||
if (rowData['video_decision'] === 'transcode') {
|
if (rowData['media_type'] === 'movie') {
|
||||||
transcode_dec = '<i class="fa fa-server"></i> ';
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||||
}
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' (' + rowData['year'] + ')</span>'
|
||||||
$(td).html('<div><div style="float: left;"><a href="info?source=history&item_id=' + rowData['id'] + '">' + cellData + '</a></div><div style="float: right; text-align: right; padding-right: 5px;">' + transcode_dec + '<i class="fa fa-video-camera"></i></div></div>');
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'episode') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' \
|
||||||
|
(S' + rowData['parent_media_index'] + '· E' + rowData['media_index'] + ')</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else if (rowData['media_type'] === 'track') {
|
} else if (rowData['media_type'] === 'track') {
|
||||||
$(td).html('<div><div style="float: left;">' + cellData + '</div><div style="float: right; text-align: right; padding-right: 5px;"><i class="fa fa-music"></i></div></div>');
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||||
}
|
}
|
||||||
@@ -100,9 +120,41 @@ history_table_modal_options = {
|
|||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
// $('html,body').scrollTop(0);
|
// $('html,body').scrollTop(0);
|
||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
|
// Create the tooltips.
|
||||||
|
$('.transcode-tooltip').tooltip();
|
||||||
|
$('.media-type-tooltip').tooltip();
|
||||||
|
$('.thumb-tooltip').popover({
|
||||||
|
html: true,
|
||||||
|
container: '#history-modal',
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'right',
|
||||||
|
content: function () {
|
||||||
|
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px;" />';
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
"preDrawCallback": function(settings) {
|
"preDrawCallback": function(settings) {
|
||||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#history_table').on('click', 'td.modal-control', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = history_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
function showStreamDetails() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_stream_data',
|
||||||
|
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#info-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showStreamDetails();
|
||||||
|
});
|
||||||
@@ -35,7 +35,7 @@ var log_table_options = {
|
|||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
},
|
},
|
||||||
"preDrawCallback": function(settings) {
|
"preDrawCallback": function(settings) {
|
||||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ sync_table_options = {
|
|||||||
"processing": false,
|
"processing": false,
|
||||||
"serverSide": false,
|
"serverSide": false,
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"order": [ 0, 'desc'],
|
"order": [ [ 0, 'desc'], [ 1, 'asc'], [2, 'asc'] ],
|
||||||
"pageLength": 25,
|
"pageLength": 25,
|
||||||
"stateSave": true,
|
"stateSave": true,
|
||||||
"language": {
|
"language": {
|
||||||
@@ -49,7 +49,7 @@ sync_table_options = {
|
|||||||
"data": "title",
|
"data": "title",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== '') {
|
if (cellData !== '') {
|
||||||
if (rowData['metadata_type'] !== 'track') {
|
if (rowData['metadata_type'] !== '') {
|
||||||
$(td).html('<a href="info?item_id=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
$(td).html('<a href="info?item_id=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html(cellData);
|
$(td).html(cellData);
|
||||||
@@ -67,13 +67,13 @@ sync_table_options = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4],
|
"targets": [4],
|
||||||
"data": "device_name",
|
"data": "platform",
|
||||||
"className": "no-wrap hidden-xs"
|
"className": "no-wrap hidden-sm hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [5],
|
"targets": [5],
|
||||||
"data": "platform",
|
"data": "device_name",
|
||||||
"className": "no-wrap hidden-sm hidden-xs"
|
"className": "no-wrap hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [6],
|
"targets": [6],
|
||||||
|
|||||||
@@ -24,13 +24,11 @@ user_ip_table_options = {
|
|||||||
},
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"width": "15%",
|
"width": "15%",
|
||||||
"className": "no-wrap"
|
"className": "no-wrap hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [1],
|
"targets": [1],
|
||||||
"data":"ip_address",
|
"data": "ip_address",
|
||||||
"width": "15%",
|
|
||||||
"className": "modal-control no-wrap",
|
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData) {
|
if (cellData) {
|
||||||
if (isPrivateIP(cellData)) {
|
if (isPrivateIP(cellData)) {
|
||||||
@@ -46,34 +44,98 @@ user_ip_table_options = {
|
|||||||
$(td).html('n/a');
|
$(td).html('n/a');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"width": "15%"
|
"width": "15%",
|
||||||
|
"className": "no-wrap modal-control-ip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [2],
|
"targets": [2],
|
||||||
"data":"play_count",
|
"data": "platform",
|
||||||
"width": "10%",
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
"className": "hidden-xs"
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "15%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [3],
|
"targets": [3],
|
||||||
"data":"platform",
|
"data":"player",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData) {
|
||||||
|
var transcode_dec = '';
|
||||||
|
if (rowData['video_decision'] === 'transcode') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span> ';
|
||||||
|
} else if (rowData['video_decision'] === 'copy') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span> ';
|
||||||
|
} else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span> ';
|
||||||
|
}
|
||||||
|
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
},
|
||||||
"width": "15%",
|
"width": "15%",
|
||||||
"className": "hidden-xs"
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4],
|
"targets": [4],
|
||||||
"data":"last_watched",
|
"data":"last_watched",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
var media_type = '';
|
||||||
|
var thumb_popover = ''
|
||||||
|
if (rowData['media_type'] === 'movie') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'episode') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'track') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type']) {
|
||||||
|
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"width": "30%",
|
"width": "30%",
|
||||||
"className": "hidden-sm hidden-xs"
|
"className": "hidden-sm hidden-xs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [5],
|
||||||
|
"data":"play_count",
|
||||||
|
"searchable": false,
|
||||||
|
"width": "10%"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"drawCallback": function (settings) {
|
"drawCallback": function (settings) {
|
||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
// $('html,body').scrollTop(0);
|
// $('html,body').scrollTop(0);
|
||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
|
// Create the tooltips.
|
||||||
|
$('.transcode-tooltip').tooltip();
|
||||||
|
$('.media-type-tooltip').tooltip();
|
||||||
|
$('.watched-tooltip').tooltip();
|
||||||
|
$('.thumb-tooltip').popover({
|
||||||
|
html: true,
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'right',
|
||||||
|
content: function () {
|
||||||
|
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
"preDrawCallback": function(settings) {
|
"preDrawCallback": function(settings) {
|
||||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +145,25 @@ $('#user_ip_table').on('mouseenter', 'td.modal-control span', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#user_ip_table').on('click', 'td.modal-control', function () {
|
$('#user_ip_table').on('click', 'td.modal-control', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = user_ip_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
function showStreamDetails() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_stream_data',
|
||||||
|
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#info-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showStreamDetails();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#user_ip_table').on('click', 'td.modal-control-ip', function () {
|
||||||
var tr = $(this).parents('tr');
|
var tr = $(this).parents('tr');
|
||||||
var row = user_ip_table.row( tr );
|
var row = user_ip_table.row( tr );
|
||||||
var rowData = row.data();
|
var rowData = row.data();
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
var users_to_delete = [];
|
||||||
|
var users_to_purge = [];
|
||||||
|
|
||||||
users_list_table_options = {
|
users_list_table_options = {
|
||||||
"language": {
|
"language": {
|
||||||
"search": "Search: ",
|
"search": "Search: ",
|
||||||
@@ -11,57 +14,164 @@ users_list_table_options = {
|
|||||||
"processing": false,
|
"processing": false,
|
||||||
"serverSide": true,
|
"serverSide": true,
|
||||||
"pageLength": 10,
|
"pageLength": 10,
|
||||||
"order": [ 1, 'asc'],
|
"order": [ 2, 'asc'],
|
||||||
"autoWidth": true,
|
"autoWidth": true,
|
||||||
"stateSave": true,
|
"stateSave": true,
|
||||||
"pagingType": "bootstrap",
|
"pagingType": "bootstrap",
|
||||||
"columnDefs": [
|
"columnDefs": [
|
||||||
{
|
{
|
||||||
"targets": [0],
|
"targets": [0],
|
||||||
"data": "thumb",
|
"data": null,
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData === '') {
|
$(td).html('<div class="edit-user-toggles"><button class="btn btn-xs btn-warning delete-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button> ' +
|
||||||
$(td).html('<img src="interfaces/default/images/gravatar-default-80x80.png" alt="User Logo"/>');
|
'<button class="btn btn-xs btn-warning purge-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>   ' +
|
||||||
} else {
|
'<input type="checkbox" id="do_notify-' + rowData['user_id'] + '" name="do_notify" value="1" ' + rowData['do_notify'] + '><label class="edit-tooltip" for="do_notify-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle Notifications"><i class="fa fa-bell fa-lg fa-fw"></i></label> ' +
|
||||||
$(td).html('<img src="' + cellData + '" alt="User Logo"/>');
|
'<input type="checkbox" id="keep_history-' + rowData['user_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label> ');
|
||||||
}
|
// Show/hide user currently doesn't work
|
||||||
|
//'<input type="checkbox" id="show_hide-' + rowData['user_id'] + '" name="show_hide" value="1" checked><label class="edit-tooltip" for="show_hide-' + rowData['user_id'] + '" data-toggle="tooltip" title="Show/Hide User"><i class="fa fa-eye fa-lg fa-fw"></i></label>');
|
||||||
},
|
},
|
||||||
"orderable": false,
|
"width": "7%",
|
||||||
"className": "users-poster-face",
|
"className": "edit-control no-wrap hidden",
|
||||||
"width": "40px"
|
"searchable": false,
|
||||||
|
"orderable": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [1],
|
"targets": [1],
|
||||||
|
"data": "user_thumb",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData === '') {
|
||||||
|
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(interfaces/default/images/gravatar-default-80x80.png);"></div></a>');
|
||||||
|
} else {
|
||||||
|
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(' + cellData + ');"></div></a>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"orderable": false,
|
||||||
|
"searchable": false,
|
||||||
|
"width": "5%",
|
||||||
|
"className": "users-thumbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [2],
|
||||||
"data": "friendly_name",
|
"data": "friendly_name",
|
||||||
"createdCell": function (td, cellData, rowData, row, col) {
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
if (cellData !== '') {
|
if (cellData !== '') {
|
||||||
if (rowData['user_id'] > 0) {
|
if (rowData['user_id'] > 0) {
|
||||||
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
|
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '"><a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>' +
|
||||||
|
'<input type="text" class="hidden" value="' + cellData + '"></div>');
|
||||||
} else {
|
} else {
|
||||||
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>');
|
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '"><a href="user?user=' + rowData['user'] + '">' + cellData + '</a>' +
|
||||||
|
'<input type="text" class="hidden" value="' + cellData + '"></div>');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$(td).html(cellData);
|
$(td).html(cellData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
"width": "10%",
|
||||||
{
|
"className": "edit-user-control no-wrap"
|
||||||
"targets": [2],
|
|
||||||
"data": "started",
|
|
||||||
"render": function ( data, type, full ) {
|
|
||||||
return moment(data, "X").fromNow();
|
|
||||||
},
|
|
||||||
"className": "hidden-xs",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [3],
|
"targets": [3],
|
||||||
"data": "ip_address",
|
"data": "last_seen",
|
||||||
|
"render": function ( data, type, full ) {
|
||||||
|
if (data) {
|
||||||
|
return moment(data, "X").fromNow();
|
||||||
|
} else {
|
||||||
|
return "never";
|
||||||
|
}
|
||||||
|
},
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"className": "hidden-xs",
|
"width": "10%",
|
||||||
|
"className": "no-wrap hidden-xs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4],
|
"targets": [4],
|
||||||
"data": "plays"
|
"data": "ip_address",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData) {
|
||||||
|
if (isPrivateIP(cellData)) {
|
||||||
|
if (cellData != '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><span data-toggle="ip-tooltip" data-placement="left" title="IP Address Info" id="ip-info"><i class="fa fa-map-marker"></i></span> ' + cellData + '</a>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [5],
|
||||||
|
"data": "platform",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html(cellData);
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "10%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [6],
|
||||||
|
"data":"player",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData) {
|
||||||
|
var transcode_dec = '';
|
||||||
|
if (rowData['video_decision'] === 'transcode') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
|
||||||
|
} else if (rowData['video_decision'] === 'copy') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>';
|
||||||
|
} else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') {
|
||||||
|
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
|
||||||
|
}
|
||||||
|
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "15%",
|
||||||
|
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [7],
|
||||||
|
"data":"last_watched",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
var media_type = '';
|
||||||
|
var thumb_popover = ''
|
||||||
|
if (rowData['media_type'] === 'movie') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'episode') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type'] === 'track') {
|
||||||
|
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||||
|
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
|
||||||
|
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></a></div>');
|
||||||
|
} else if (rowData['media_type']) {
|
||||||
|
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||||
|
} else {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"width": "30%",
|
||||||
|
"className": "hidden-sm hidden-xs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [8],
|
||||||
|
"data": "plays",
|
||||||
|
"searchable": false,
|
||||||
|
"width": "10%"
|
||||||
}
|
}
|
||||||
|
|
||||||
],
|
],
|
||||||
@@ -69,9 +179,153 @@ users_list_table_options = {
|
|||||||
// Jump to top of page
|
// Jump to top of page
|
||||||
//$('html,body').scrollTop(0);
|
//$('html,body').scrollTop(0);
|
||||||
$('#ajaxMsg').fadeOut();
|
$('#ajaxMsg').fadeOut();
|
||||||
|
|
||||||
|
// Create the tooltips.
|
||||||
|
$('.purge-tooltip').tooltip();
|
||||||
|
$('.edit-tooltip').tooltip();
|
||||||
|
$('.transcode-tooltip').tooltip();
|
||||||
|
$('.media-type-tooltip').tooltip();
|
||||||
|
$('.watched-tooltip').tooltip();
|
||||||
|
$('.thumb-tooltip').popover({
|
||||||
|
html: true,
|
||||||
|
trigger: 'hover',
|
||||||
|
placement: 'right',
|
||||||
|
content: function () {
|
||||||
|
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($('#row-edit-mode').hasClass('active')) {
|
||||||
|
$('.edit-control').each(function () {
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"preDrawCallback": function(settings) {
|
"preDrawCallback": function(settings) {
|
||||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
var msg = "<i class='fa fa-refresh fa-spin'></i> Fetching rows...";
|
||||||
showMsg(msg, false, false, 0)
|
showMsg(msg, false, false, 0)
|
||||||
|
},
|
||||||
|
"rowCallback": function (row, rowData) {
|
||||||
|
if ($.inArray(rowData['user_id'], users_to_purge) !== -1) {
|
||||||
|
$(row).find('button[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#users_list_table').on('click', 'td.modal-control', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = users_list_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
function showStreamDetails() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_stream_data',
|
||||||
|
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#info-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showStreamDetails();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#users_list_table').on('click', 'td.modal-control-ip', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = users_list_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
function getUserLocation(ip_address) {
|
||||||
|
if (isPrivateIP(ip_address)) {
|
||||||
|
return "n/a"
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_ip_address_details',
|
||||||
|
data: { ip_address: ip_address },
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#ip-info-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserLocation(rowData['ip_address']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > input, td.edit-user-control > .edit-user-name > input', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = users_list_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
var do_notify = 0;
|
||||||
|
var keep_history = 0;
|
||||||
|
if ($('#do_notify-' + rowData['user_id']).is(':checked')) {
|
||||||
|
do_notify = 1;
|
||||||
|
}
|
||||||
|
if ($('#keep_history-' + rowData['user_id']).is(':checked')) {
|
||||||
|
keep_history = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
friendly_name = tr.find('td.edit-user-control > .edit-user-name > input').val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'edit_user',
|
||||||
|
data: {
|
||||||
|
user_id: rowData['user_id'],
|
||||||
|
friendly_name: friendly_name,
|
||||||
|
do_notify: do_notify,
|
||||||
|
keep_history: keep_history,
|
||||||
|
thumb: rowData['user_thumb']
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "User updated";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > button.delete-user', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = users_list_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
|
||||||
|
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
|
||||||
|
|
||||||
|
if (index_delete === -1) {
|
||||||
|
users_to_delete.push(rowData['user_id']);
|
||||||
|
if (index_purge === -1) {
|
||||||
|
tr.find('button.purge-user').click();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
users_to_delete.splice(index_delete, 1);
|
||||||
|
if (index_purge != -1) {
|
||||||
|
tr.find('button.purge-user').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > button.purge-user', function () {
|
||||||
|
var tr = $(this).parents('tr');
|
||||||
|
var row = users_list_table.row(tr);
|
||||||
|
var rowData = row.data();
|
||||||
|
|
||||||
|
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
|
||||||
|
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
|
||||||
|
|
||||||
|
if (index_purge === -1) {
|
||||||
|
users_to_purge.push(rowData['user_id']);
|
||||||
|
} else {
|
||||||
|
users_to_purge.splice(index_purge, 1);
|
||||||
|
if (index_delete != -1) {
|
||||||
|
tr.find('button.delete-user').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
});
|
||||||
84
data/interfaces/default/library_stats.html
Normal file
84
data/interfaces/default/library_stats.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<%doc>
|
||||||
|
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
|
||||||
|
|
||||||
|
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
|
||||||
|
|
||||||
|
Filename: library_stats.html
|
||||||
|
Version: 0.1
|
||||||
|
Variable names: data [array]
|
||||||
|
|
||||||
|
data[array_index] :: Usable parameters
|
||||||
|
|
||||||
|
data['type'] Returns the type of the library. Either 'movie', 'show', 'photo', or 'artist'.
|
||||||
|
data['rows'] Returns an array containing stat data
|
||||||
|
|
||||||
|
data[array_index]['rows'] :: Usable parameters
|
||||||
|
|
||||||
|
title Returns the title of the library.
|
||||||
|
thumb Returns the thumb of the library.
|
||||||
|
count Returns the number of items in the library.
|
||||||
|
count_type Returns the sorting type for the library
|
||||||
|
|
||||||
|
== Only if 'type' is 'show'
|
||||||
|
episode_count Return the number of episodes in the library.
|
||||||
|
episode_count_type Return the sorting type for the episodes.
|
||||||
|
|
||||||
|
== Only if 'type' is 'artist'
|
||||||
|
album_count Return the number of episodes in the library.
|
||||||
|
album_count_type Return the sorting type for the episodes.
|
||||||
|
|
||||||
|
DOCUMENTATION :: END
|
||||||
|
</%doc>
|
||||||
|
|
||||||
|
% if data:
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
% for library in data:
|
||||||
|
<div class="home-platforms-instance">
|
||||||
|
<li>
|
||||||
|
<div class="home-platforms-instance-info">
|
||||||
|
<div class="home-platforms-instance-name">
|
||||||
|
% if library['type'] != 'photo':
|
||||||
|
<h4>
|
||||||
|
<a href="info?item_id=${library['type']}" title="${library['rows']['title']}">${library['rows']['title']}</a>
|
||||||
|
</h4>
|
||||||
|
% else:
|
||||||
|
<h4>${library['rows']['title']}</h4>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="home-platforms-instance-playcount">
|
||||||
|
<h5>${library['rows']['count_type']}</h5>
|
||||||
|
<h3>${library['rows']['count']}</h3>
|
||||||
|
<p> items</p>
|
||||||
|
</div>
|
||||||
|
% if library['type'] == 'show':
|
||||||
|
<div class="home-platforms-instance-playcount" style="padding-left: 10px;">
|
||||||
|
<h5>${library['rows']['episode_count_type']}</h5>
|
||||||
|
<h3>${library['rows']['episode_count']}</h3>
|
||||||
|
<p> items</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
% if library['type'] == 'artist':
|
||||||
|
<div class="home-platforms-instance-playcount" style="padding-left: 10px;">
|
||||||
|
<h5>${library['rows']['album_count_type']}</h5>
|
||||||
|
<h3>${library['rows']['album_count']}</h3>
|
||||||
|
<p> items</p>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
% if library['rows']['thumb']:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-library-thumb" style="background-image: url(pms_image_proxy?img=${library['rows']['thumb']}&width=300&height=300&fallback=poster);"></div>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="home-platforms-instance-poster">
|
||||||
|
<div class="home-platforms-library-thumb" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
% else:
|
||||||
|
<div class="text-muted">Unable to retrieve data from server. Please check your <a href="settings">settings</a>.
|
||||||
|
</div><br>
|
||||||
|
% endif
|
||||||
@@ -86,6 +86,7 @@ from plexpy import helpers
|
|||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
LoadPlexPyLogs();
|
LoadPlexPyLogs();
|
||||||
|
clearSearchButton('log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
function LoadPlexPyLogs() {
|
function LoadPlexPyLogs() {
|
||||||
@@ -105,11 +106,13 @@ from plexpy import helpers
|
|||||||
$("#plexpy-logs-btn").click(function() {
|
$("#plexpy-logs-btn").click(function() {
|
||||||
$("#clear-logs").show();
|
$("#clear-logs").show();
|
||||||
LoadPlexPyLogs();
|
LoadPlexPyLogs();
|
||||||
|
clearSearchButton('log_table', log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#plex-logs-btn").click(function() {
|
$("#plex-logs-btn").click(function() {
|
||||||
$("#clear-logs").hide();
|
$("#clear-logs").hide();
|
||||||
LoadPlexLogs();
|
LoadPlexLogs();
|
||||||
|
clearSearchButton('plex_log_table', plex_log_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#clear-logs").click(function() {
|
$("#clear-logs").click(function() {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
<%!
|
||||||
|
from plexpy import helpers
|
||||||
|
%>
|
||||||
% if data:
|
% if data:
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -9,23 +12,55 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<form action="set_notification_config" method="post" class="form" id="set_notification_config" data-parsley-validate>
|
<form action="set_notification_config" method="post" class="form" id="set_notification_config" data-parsley-validate>
|
||||||
<div class="col-md-6">
|
<div class="col-md-12">
|
||||||
% for item in data:
|
% for item in data:
|
||||||
% if item['input_type'] != 'checkbox':
|
% if item['input_type'] == 'text' or item['input_type'] == 'number' or item['input_type'] == 'password':
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${item['name']}">${item['label']}</label>
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
|
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
|
||||||
% if item['name'] == 'osx_notify_app':
|
% if item['name'] == 'osx_notify_app':
|
||||||
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
|
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
|
||||||
% endif
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description']}</p>
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'button':
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="${item['input_type']}" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="help-block">${item['description']}</p>
|
<p class="help-block">${item['description']}</p>
|
||||||
</div>
|
</div>
|
||||||
% elif item['input_type'] == 'checkbox':
|
% elif item['input_type'] == 'checkbox':
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="${item['name']}" name="${item['name']}" value="1" ${item['value']}> ${item['label']}
|
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${helpers.checked(item['value'])}> ${item['label']}
|
||||||
</label>
|
</label>
|
||||||
<p class="help-block">${item['description']}</p>
|
<p class="help-block">${item['description']}</p>
|
||||||
|
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
|
||||||
|
</div>
|
||||||
|
% elif item['input_type'] == 'select':
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${item['name']}">${item['label']}</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<select class="form-control" id="${item['name']}" name="${item['name']}" >
|
||||||
|
% for key, value in sorted(item['select_options'].iteritems()):
|
||||||
|
% if key == item['value']:
|
||||||
|
<option value="${key}" selected>${value}</option>
|
||||||
|
% else:
|
||||||
|
<option value="${key}">${value}</option>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="help-block">${item['description']}</p>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
@@ -35,7 +70,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
<%
|
||||||
|
nosave = any(d['input_type'] == 'nosave' for d in data)
|
||||||
|
%>
|
||||||
|
% if not nosave:
|
||||||
<input type="button" id="save-notification-item" class="btn btn-bright" value="Save">
|
<input type="button" id="save-notification-item" class="btn btn-bright" value="Save">
|
||||||
|
% else:
|
||||||
|
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,13 +86,54 @@
|
|||||||
<script>
|
<script>
|
||||||
$('#osxnotifyregister').click(function () {
|
$('#osxnotifyregister').click(function () {
|
||||||
var osx_notify_app = $("#osx_notify_app").val();
|
var osx_notify_app = $("#osx_notify_app").val();
|
||||||
$.get("/osxnotifyregister", {'app': osx_notify_app}, function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
|
$.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
|
||||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut()
|
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||||
})
|
})
|
||||||
|
|
||||||
var notificationConfig = $("#set_notification_config");
|
|
||||||
$('#save-notification-item').click(function() {
|
$('#save-notification-item').click(function() {
|
||||||
doAjaxCall('set_notification_config',$(this),'tabs',true);
|
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
||||||
|
// Reload modal to update certain fields
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_notification_agent_config',
|
||||||
|
data: { config_id: '${config_id}' },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#notification-config-modal").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#twitterStep1').click(function () {
|
||||||
|
$.get("/twitterStep1", function (data) {window.open(data); })
|
||||||
|
.done(function () { $('#ajaxMsg').html("<i class='fa fa-check'></i> Confirm Authorization. Check pop-up blocker if no response."); });
|
||||||
|
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||||
|
});
|
||||||
|
$('#twitterStep2').click(function () {
|
||||||
|
var twitter_key = $("#twitter_key").val();
|
||||||
|
$.get("/twitterStep2", { 'key': twitter_key }, function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
|
||||||
|
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||||
|
});
|
||||||
|
$('#testTwitter').click(function () {
|
||||||
|
$.get("/testTwitter",
|
||||||
|
function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
|
||||||
|
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#testIFTTT').click(function () {
|
||||||
|
$.get("/test_ifttt",
|
||||||
|
function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
|
||||||
|
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Never send checkbox values directly, always substitute value in hidden input.
|
||||||
|
$('.checkboxes').click(function() {
|
||||||
|
var configToggle = $(this).data('id');
|
||||||
|
if ($(this).is(":checked")) {
|
||||||
|
$("#"+configToggle).val(1);
|
||||||
|
} else {
|
||||||
|
$("#"+configToggle).val(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
123
data/interfaces/default/notification_triggers_modal.html
Normal file
123
data/interfaces/default/notification_triggers_modal.html
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<%!
|
||||||
|
from plexpy import helpers
|
||||||
|
%>
|
||||||
|
% if data:
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="notification-triggers-modal-header">${data['name']} Notification Triggers</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<p class="help-block">
|
||||||
|
Watched notifications are only applicable for video items.
|
||||||
|
</p>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_play" ${helpers.checked(data['on_play'])} class="toggle-switches">
|
||||||
|
Notify on playback start
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a new media item is started.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_stop" ${helpers.checked(data['on_stop'])} class="toggle-switches">
|
||||||
|
Notify on playback stop
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a media item is stopped.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_pause" ${helpers.checked(data['on_pause'])} class="toggle-switches">
|
||||||
|
Notify on playback pause
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a media item is paused.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_resume" ${helpers.checked(data['on_resume'])} class="toggle-switches">
|
||||||
|
Notify on playback resume
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a media item is resumed from a paused state.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_watched" ${helpers.checked(data['on_watched'])} class="toggle-switches">
|
||||||
|
Notify on watched
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a video item reaches the defined watch percentage.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_buffer" ${helpers.checked(data['on_buffer'])} class="toggle-switches">
|
||||||
|
Notify on buffer warning
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a media item triggers the defined buffer threshold.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_created" ${helpers.checked(data['on_created'])} class="toggle-switches">
|
||||||
|
Notify on recently added
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when a media item is added to the Plex Media Server.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extdown" ${helpers.checked(data['on_extdown'])} class="toggle-switches">
|
||||||
|
Notify on Plex remote access down
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached externally.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intdown" ${helpers.checked(data['on_intdown'])} class="toggle-switches">
|
||||||
|
Notify on Plex server down
|
||||||
|
</label>
|
||||||
|
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached internally.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$('.toggle-switches').click(function() {
|
||||||
|
var configToggle = $(this).data('id');
|
||||||
|
var toggle = $(this);
|
||||||
|
if ($(this).is(":checked")) {
|
||||||
|
var data = {};
|
||||||
|
data[$(this).data('config-name')] = 1;
|
||||||
|
$.ajax({
|
||||||
|
url: 'set_notification_config',
|
||||||
|
data: data,
|
||||||
|
async: true,
|
||||||
|
success: function(data) {
|
||||||
|
console.log('success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('.toggle-notification-triggers-modal[data-id=' + configToggle + ']').addClass('active');
|
||||||
|
} else {
|
||||||
|
var data = {};
|
||||||
|
data[$(this).data('config-name')] = 0;
|
||||||
|
$.ajax({
|
||||||
|
url: 'set_notification_config',
|
||||||
|
data: data,
|
||||||
|
async: true,
|
||||||
|
success: function(data) {
|
||||||
|
console.log('success');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!($('.toggle-switches').is(":checked"))) {
|
||||||
|
$('.toggle-notification-triggers-modal[data-id=' + configToggle + ']').removeClass('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
@@ -11,12 +11,13 @@ data[array_index] :: Usable parameters
|
|||||||
|
|
||||||
== Global keys ==
|
== Global keys ==
|
||||||
rating_key Returns the unique identifier for the media item.
|
rating_key Returns the unique identifier for the media item.
|
||||||
type Returns the type of media. Either 'movie' or 'season'.
|
media_type Returns the media type of media. Either 'movie' or 'season' or 'album'.
|
||||||
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||||
added_at Returns the time when the media was added to the library.
|
added_at Returns the time when the media was added to the library.
|
||||||
title Returns the name of the movie or season.
|
title Returns the name of the movie or season.
|
||||||
|
parent_title Returns the name of the TV Show a season belongs too.
|
||||||
|
|
||||||
== Only if 'type' is 'movie' ==
|
== Only if 'media_type' is 'movie' ==
|
||||||
year Returns the movie release year.
|
year Returns the movie release year.
|
||||||
|
|
||||||
DOCUMENTATION :: END
|
DOCUMENTATION :: END
|
||||||
@@ -28,32 +29,50 @@ DOCUMENTATION :: END
|
|||||||
% for item in data:
|
% for item in data:
|
||||||
<div class="dashboard-recent-media-instance">
|
<div class="dashboard-recent-media-instance">
|
||||||
<li>
|
<li>
|
||||||
<div class="poster">
|
% if item['media_type'] == 'season' or item['media_type'] == 'movie':
|
||||||
% if item['type'] == 'season' or item['type'] == 'movie':
|
|
||||||
<div class="poster-face">
|
|
||||||
<a href="info?item_id=${item['rating_key']}">
|
<a href="info?item_id=${item['rating_key']}">
|
||||||
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
|
<div class="dashboard-recent-media-poster">
|
||||||
</a>
|
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||||
|
<div class="dashboard-recent-media-overlay">
|
||||||
|
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||||
|
<script>
|
||||||
|
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% elif item['type'] == 'album':
|
|
||||||
<div class="cover-face">
|
|
||||||
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover" class="cover-face">
|
|
||||||
</div>
|
</div>
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-recent-media-metacontainer">
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
% if item['type'] == 'season' or item['type'] == 'album':
|
% if item['media_type'] == 'season':
|
||||||
<h3>${item['title']}</h3>
|
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||||
% elif item['type'] == 'movie':
|
<h3 class="text-muted">${item['title']}</h3>
|
||||||
<h3>${item['title']} (${item['year']})</h3>
|
% elif item['media_type'] == 'movie':
|
||||||
|
<h3 title="${item['title']}">${item['title']}</h3>
|
||||||
|
<h3 class="text-muted">${item['year']}</h3>
|
||||||
% endif
|
% endif
|
||||||
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
% elif item['media_type'] == 'album':
|
||||||
|
<a href="info?item_id=${item['rating_key']}">
|
||||||
|
<div class="dashboard-recent-media-cover">
|
||||||
|
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||||
|
<div class="dashboard-recent-media-overlay">
|
||||||
|
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
|
||||||
|
<script>
|
||||||
|
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
|
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
|
||||||
|
<h3 class="text-muted">${item['title']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
% endif
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
$('#added_at-${item['rating_key']}').html('Added ' + moment(${item['added_at']}, "X").fromNow())
|
|
||||||
</script>
|
|
||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
41
data/interfaces/default/search.html
Normal file
41
data/interfaces/default/search.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<%inherit file="base.html"/>
|
||||||
|
|
||||||
|
<%def name="headIncludes()">
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="headerIncludes()">
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="body()">
|
||||||
|
<div class='container-fluid'>
|
||||||
|
<div class='table-card-header'>
|
||||||
|
<div class="header-bar">
|
||||||
|
<span><i class="fa fa-search"></i> Search Results
|
||||||
|
% if query:
|
||||||
|
for <strong>${query}</strong>
|
||||||
|
% endif
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='table-card-back'>
|
||||||
|
<div id="search-results-list"><i class="fa fa-refresh fa-spin"></i> Loading search results...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="javascriptIncludes()">
|
||||||
|
<script>
|
||||||
|
$('#search_button').removeClass('btn-inactive');
|
||||||
|
$('#query').val("${query.replace('"','\\"')}").css({ right: '0', width: '250px' }).addClass('active');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_search_results_children',
|
||||||
|
type: "GET",
|
||||||
|
async: true,
|
||||||
|
data: {'query': "${query.replace('"','\\"')}"},
|
||||||
|
complete: function (xhr, status) {
|
||||||
|
$("#search-results-list").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -54,8 +54,9 @@ DOCUMENTATION :: END
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h4><strong>Stream Details</strong></h4>
|
<h4><strong>Stream Details</strong></h4>
|
||||||
|
% if data['media_type'] != 'track':
|
||||||
<h5>Video</h5>
|
<h5>Video</h5>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
% if data['transcode_video_dec'] != 'direct play':
|
% if data['transcode_video_dec'] != 'direct play':
|
||||||
<li>Stream Type: <strong>${data['transcode_video_dec']}</strong></li>
|
<li>Stream Type: <strong>${data['transcode_video_dec']}</strong></li>
|
||||||
<li>Video Resolution: <strong>${data['transcode_height']}p</strong></li>
|
<li>Video Resolution: <strong>${data['transcode_height']}p</strong></li>
|
||||||
@@ -74,8 +75,9 @@ DOCUMENTATION :: END
|
|||||||
<li>Video Height: <strong>${data['height']}</strong></li>
|
<li>Video Height: <strong>${data['height']}</strong></li>
|
||||||
% endif
|
% endif
|
||||||
</ul>
|
</ul>
|
||||||
|
% endif
|
||||||
<h5>Audio</h5>
|
<h5>Audio</h5>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
% if data['transcode_audio_dec'] != 'direct play':
|
% if data['transcode_audio_dec'] != 'direct play':
|
||||||
<li>Stream Type: <strong>${data['transcode_audio_dec']}</strong></li>
|
<li>Stream Type: <strong>${data['transcode_audio_dec']}</strong></li>
|
||||||
<li>Audio Codec: <strong>${data['transcode_audio_codec']}</strong></li>
|
<li>Audio Codec: <strong>${data['transcode_audio_codec']}</strong></li>
|
||||||
@@ -89,23 +91,27 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h4><strong>Media Source Details</strong></h4>
|
<h4><strong>Media Source Details</strong></h4>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
<li>Container: <strong>${data['container']}</strong></li>
|
<li>Container: <strong>${data['container']}</strong></li>
|
||||||
|
% if data['media_type'] != 'track':
|
||||||
<li>Resolution: <strong>${data['height']}p</strong></li>
|
<li>Resolution: <strong>${data['height']}p</strong></li>
|
||||||
|
% endif
|
||||||
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
|
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
% if data['media_type'] != 'track':
|
||||||
<h4><strong>Video Source Details</strong></h4>
|
<h4><strong>Video Source Details</strong></h4>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
<li>Width: <strong>${data['width']}</strong></li>
|
<li>Width: <strong>${data['width']}</strong></li>
|
||||||
<li>Height: <strong>${data['height']}</strong></li>
|
<li>Height: <strong>${data['height']}</strong></li>
|
||||||
<li>Aspect Ratio: <strong>${data['aspect_ratio']}</strong></li>
|
<li>Aspect Ratio: <strong>${data['aspect_ratio']}</strong></li>
|
||||||
<li>Video Frame Rate: <strong>${data['video_framerate']}</strong></li>
|
<li>Video Frame Rate: <strong>${data['video_framerate']}</strong></li>
|
||||||
<li>Video Codec: <strong>${data['video_codec']}</strong></li>
|
<li>Video Codec: <strong>${data['video_codec']}</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
% endif
|
||||||
<h4><strong>Audio Source Details</strong></h4>
|
<h4><strong>Audio Source Details</strong></h4>
|
||||||
<ul>
|
<ul class="list-unstyled">
|
||||||
<li>Audio Codec: <strong>${data['audio_codec']}</strong></li>
|
<li>Audio Codec: <strong>${data['audio_codec']}</strong></li>
|
||||||
<li>Audio Channels: <strong>${data['audio_channels']}</strong></li>
|
<li>Audio Channels: <strong>${data['audio_channels']}</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -26,11 +26,11 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align='left' id="state">State</th>
|
<th align='left' id="state">State</th>
|
||||||
<th align='left' id="username">Username</th>
|
<th align='left' id="user">User</th>
|
||||||
<th align='left' id="title">Title</th>
|
<th align='left' id="title">Title</th>
|
||||||
<th align='left' id="type">Type</th>
|
<th align='left' id="type">Type</th>
|
||||||
<th align='left' id="device">Device</th>
|
|
||||||
<th align='left' id="platform">Platform</th>
|
<th align='left' id="platform">Platform</th>
|
||||||
|
<th align='left' id="device">Device</th>
|
||||||
<th align='left' id="size">Total Size</th>
|
<th align='left' id="size">Total Size</th>
|
||||||
<th align='left' id="items">Total Items</th>
|
<th align='left' id="items">Total Items</th>
|
||||||
<th align='left' id="converted">Converted</th>
|
<th align='left' id="converted">Converted</th>
|
||||||
@@ -57,9 +57,10 @@
|
|||||||
"url": "get_sync"
|
"url": "get_sync"
|
||||||
}
|
}
|
||||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||||
|
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||||
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
|
|
||||||
$( colvis.button() ).appendTo('div.colvis-button-bar');
|
$( colvis.button() ).appendTo('div.colvis-button-bar');
|
||||||
|
|
||||||
|
clearSearchButton('sync_table', sync_table);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ from plexpy import helpers
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<div class="user-info-wrapper">
|
<div class="user-info-wrapper">
|
||||||
<div class="user-info-poster-face" id="user-gravatar">
|
<div class="user-info-poster-face" id="user-gravatar" style="background-image: url(${data['thumb']});">
|
||||||
<img src="${data['thumb']}" height="80px" width="80px">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-username">
|
<div class="user-info-username">
|
||||||
<span class="set-username">${data['friendly_name']}</span> <span id="edit-user-tooltip" data-target="tooltip" title="Edit user details"><a href="#" data-toggle="modal" data-target="#edit-user-modal" id="toggle-edit-user-modal"><i class="fa fa-pencil"></i></a></span>
|
<span class="set-username">${data['friendly_name']}</span> <span id="edit-user-tooltip" data-target="tooltip" title="Edit user details"><a href="#" data-toggle="modal" data-target="#edit-user-modal" id="toggle-edit-user-modal"><i class="fa fa-pencil"></i></a></span>
|
||||||
@@ -86,11 +85,11 @@ from plexpy import helpers
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-header">
|
<div class="table-card-header">
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-television"></i> Platform Stats</span>
|
<span><i class="fa fa-television"></i> Player Stats</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<div id="user-platform-stats" class="user-platforms">
|
<div id="user-player-stats" class="user-player">
|
||||||
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div>
|
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading data...</div>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +102,7 @@ from plexpy import helpers
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="table-card-header">
|
<div class="table-card-header">
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-history"></i> Recently watched</span>
|
<span><i class="fa fa-history"></i> Recently Watched</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
@@ -131,17 +130,16 @@ from plexpy import helpers
|
|||||||
<table id="user_ip_table" class="display" width="100%">
|
<table id="user_ip_table" class="display" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left">Last seen</th>
|
<th align="left">Last Seen</th>
|
||||||
<th align="left">IP Address</th>
|
<th align="left">IP Address</th>
|
||||||
<th align="left">Play Count</th>
|
<th align="left">Last Platform</th>
|
||||||
<th align="left">Platform (Last Seen)</th>
|
<th align="left">Last Player</th>
|
||||||
<th align="left">Last Watched</th>
|
<th align="left">Last Watched</th>
|
||||||
|
<th align="left">Play Count</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,30 +154,36 @@ from plexpy import helpers
|
|||||||
<span class="set-username">${data['friendly_name']}</span>
|
<span class="set-username">${data['friendly_name']}</span>
|
||||||
</strong></span>
|
</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
|
<div class="button-bar">
|
||||||
|
<div class="colvis-button-bar hidden-xs" id="button-bar-history"></div>
|
||||||
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||||
|
<i class="fa fa-trash-o"></i> Delete mode
|
||||||
|
</button>
|
||||||
|
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select rows to delete. Data is deleted upon exiting delete mode.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-card-back">
|
<div class="table-card-back">
|
||||||
<table class="display" id="history_table" width="100%">
|
<table class="display" id="history_table" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th align='left' id="delete">Delete</th>
|
||||||
<th align='left' id="time">Time</th>
|
<th align='left' id="time">Time</th>
|
||||||
<th align='left' id="friendly_name">User</th>
|
<th align='left' id="friendly_name">User</th>
|
||||||
<th align='left' id="platform">Platform</th>
|
|
||||||
<th align='left' id="ip_address">IP Address</th>
|
<th align='left' id="ip_address">IP Address</th>
|
||||||
|
<th align='left' id="platform">Platform</th>
|
||||||
|
<th align='left' id="player">Player</th>
|
||||||
<th align='left' id="title">Title</th>
|
<th align='left' id="title">Title</th>
|
||||||
<th align='left' id="started">Started</th>
|
<th align='left' id="started">Started</th>
|
||||||
<th align='left' id="paused_counter">Paused</th>
|
<th align='left' id="paused_counter">Paused</th>
|
||||||
<th align='left' id="stopped">Stopped</th>
|
<th align='left' id="stopped">Stopped</th>
|
||||||
<th align='left' id="duration">Duration</th>
|
<th align='left' id="duration">Duration</th>
|
||||||
<th align='left' id="percent_complete">Watched</th>
|
<th align='left' id="percent_complete"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,8 +209,8 @@ from plexpy import helpers
|
|||||||
<th align='left' id="username">Username</th>
|
<th align='left' id="username">Username</th>
|
||||||
<th align='left' id="sync_title">Title</th>
|
<th align='left' id="sync_title">Title</th>
|
||||||
<th align='left' id="type">Type</th>
|
<th align='left' id="type">Type</th>
|
||||||
<th align='left' id="device">Device</th>
|
|
||||||
<th align='left' id="sync_platform">Platform</th>
|
<th align='left' id="sync_platform">Platform</th>
|
||||||
|
<th align='left' id="device">Device</th>
|
||||||
<th align='left' id="size">Total Size</th>
|
<th align='left' id="size">Total Size</th>
|
||||||
<th align='left' id="items">Total Items</th>
|
<th align='left' id="items">Total Items</th>
|
||||||
<th align='left' id="converted">Converted</th>
|
<th align='left' id="converted">Converted</th>
|
||||||
@@ -222,6 +226,28 @@ from plexpy import helpers
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Confirm Delete</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
|
||||||
|
<p>This is permanent and cannot be undone!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer></footer>
|
<footer></footer>
|
||||||
</%def>
|
</%def>
|
||||||
@@ -244,13 +270,15 @@ from plexpy import helpers
|
|||||||
var user_id = null;
|
var user_id = null;
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
var username = '${data['username'].replace("'", "\\'")}';
|
||||||
|
|
||||||
$("#edit-user-tooltip").tooltip();
|
$("#edit-user-tooltip").tooltip();
|
||||||
|
|
||||||
// Populate watch time stats
|
// Populate watch time stats
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_user_watch_time_stats',
|
url: 'get_user_watch_time_stats',
|
||||||
async: true,
|
async: true,
|
||||||
data: { user_id: user_id, user: '${data['username']}' },
|
data: { user_id: user_id, user: username },
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#user-time-stats").html(xhr.responseText);
|
$("#user-time-stats").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
@@ -258,41 +286,63 @@ from plexpy import helpers
|
|||||||
|
|
||||||
// Populate platform stats
|
// Populate platform stats
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_user_platform_stats',
|
url: 'get_user_player_stats',
|
||||||
async: true,
|
async: true,
|
||||||
data: { user_id: user_id, user: '${data['username']}' },
|
data: { user_id: user_id, user: username },
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
$("#user-platform-stats").html(xhr.responseText);
|
$("#user-player-stats").html(xhr.responseText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate recently watched
|
function loadHistoryTable(media_type) {
|
||||||
$.ajax({
|
|
||||||
url: 'get_user_recently_watched',
|
|
||||||
async: true,
|
|
||||||
data: { user_id: user_id, user: '${data['username']}' },
|
|
||||||
complete: function(xhr, status) {
|
|
||||||
$("#user-recently-watched").html(xhr.responseText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$( "#history-tab-btn" ).one( "click", function() {
|
|
||||||
// Build watch history table
|
// Build watch history table
|
||||||
history_table_options.ajax = {
|
history_table_options.ajax = {
|
||||||
"url": "get_history",
|
url: 'get_history',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return { 'json_data': JSON.stringify( d ),
|
return {
|
||||||
|
'json_data': JSON.stringify( d ),
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'user': "${data['username']}"
|
'user': username,
|
||||||
|
'media_type': media_type
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
history_table = $('#history_table').DataTable(history_table_options);
|
history_table = $('#history_table').DataTable(history_table_options);
|
||||||
history_table.column(1).visible(false);
|
history_table.column(2).visible(false);
|
||||||
|
|
||||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||||
$(colvis.button()).appendTo('#button-bar-history');
|
$(colvis.button()).appendTo('#button-bar-history');
|
||||||
|
|
||||||
|
clearSearchButton('history_table', history_table);
|
||||||
|
|
||||||
|
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
|
||||||
|
<label class="btn btn-dark active"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-all" value="all" autocomplete="off"> All \
|
||||||
|
</label> \
|
||||||
|
<label class="btn btn-dark"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-movies" value="movie" autocomplete="off"> Movies \
|
||||||
|
</label> \
|
||||||
|
<label class="btn btn-dark"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-tv_shows" value="episode" autocomplete="off"> TV Shows \
|
||||||
|
</label> \
|
||||||
|
<label class="btn btn-dark"> \
|
||||||
|
<input type="radio" name="media_type-filter" id="history-music" value="track" autocomplete="off"> Music \
|
||||||
|
</label> \
|
||||||
|
</div>');
|
||||||
|
|
||||||
|
$('#media_type-selection').on('change', function () {
|
||||||
|
$('#media_type-selection > label').removeClass('active');
|
||||||
|
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
|
||||||
|
$(selected_filter).closest('label').addClass('active');
|
||||||
|
media_type = $(selected_filter).val();
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$( "#history-tab-btn" ).one( "click", function() {
|
||||||
|
var media_type = 'all';
|
||||||
|
loadHistoryTable(media_type);
|
||||||
});
|
});
|
||||||
|
|
||||||
$( "#ip-tab-btn" ).one( "click", function() {
|
$( "#ip-tab-btn" ).one( "click", function() {
|
||||||
@@ -303,11 +353,13 @@ from plexpy import helpers
|
|||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return { 'json_data': JSON.stringify( d ),
|
return { 'json_data': JSON.stringify( d ),
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'user': "${data['username']}"
|
'user': username
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
|
user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
|
||||||
|
|
||||||
|
clearSearchButton('user_ip_table', user_ip_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
$( "#sync-tab-btn" ).one( "click", function() {
|
$( "#sync-tab-btn" ).one( "click", function() {
|
||||||
@@ -316,14 +368,16 @@ from plexpy import helpers
|
|||||||
"url": "get_sync",
|
"url": "get_sync",
|
||||||
"data": function(d) {
|
"data": function(d) {
|
||||||
d.user_id = user_id;
|
d.user_id = user_id;
|
||||||
d.user = "${data['username']}";
|
d.user = username;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||||
history_table.column(1).visible(false);
|
sync_table.column(1).visible(false);
|
||||||
|
|
||||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
|
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||||
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
||||||
|
|
||||||
|
clearSearchButton('sync_table', sync_table);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load edit user modal
|
// Load edit user modal
|
||||||
@@ -331,7 +385,7 @@ from plexpy import helpers
|
|||||||
$("#edit-user-tooltip").tooltip('hide');
|
$("#edit-user-tooltip").tooltip('hide');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'edit_user_dialog',
|
url: 'edit_user_dialog',
|
||||||
data: { user_id: user_id, user: '${data['username']}' },
|
data: { user_id: user_id, user: username },
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
complete: function(xhr, status) {
|
complete: function(xhr, status) {
|
||||||
@@ -339,6 +393,75 @@ from plexpy import helpers
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#row-edit-mode').on('click', function() {
|
||||||
|
$('#row-edit-mode-alert').fadeIn(200);
|
||||||
|
|
||||||
|
if ($(this).hasClass('active')) {
|
||||||
|
if (history_to_delete.length > 0) {
|
||||||
|
$('#deleteCount').text(history_to_delete.length);
|
||||||
|
$('#confirm-modal').modal();
|
||||||
|
$('#confirm-modal').one('click', '#confirm-delete', function () {
|
||||||
|
for (var i = 0; i < history_to_delete.length; i++) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_history_rows',
|
||||||
|
data: { row_id: history_to_delete[i] },
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "History deleted";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
history_table.draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.delete-control').each(function () {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
$('#row-edit-mode-alert').fadeOut(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
history_to_delete = [];
|
||||||
|
$('.delete-control').each(function() {
|
||||||
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function recentlyWatched() {
|
||||||
|
var widthVal = $('body').find("#user-recently-watched").width();
|
||||||
|
var tmp = (widthVal-32) / 180;
|
||||||
|
|
||||||
|
if (tmp > 0) {
|
||||||
|
containerSize = parseInt(tmp);
|
||||||
|
} else {
|
||||||
|
containerSize = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
% if data['user_id']:
|
||||||
|
var user_id = ${data['user_id']};
|
||||||
|
% else:
|
||||||
|
var user_id = null;
|
||||||
|
% endif
|
||||||
|
|
||||||
|
// Populate recently watched
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_user_recently_watched',
|
||||||
|
async: true,
|
||||||
|
data: { user_id: user_id, user: username, limit: containerSize },
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#user-recently-watched").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recentlyWatched();
|
||||||
|
$(window).resize(function() {
|
||||||
|
recentlyWatched();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ data[array_index] :: Usable parameters
|
|||||||
|
|
||||||
== Global keys ==
|
== Global keys ==
|
||||||
result_id Returns a unique identifier for the result.
|
result_id Returns a unique identifier for the result.
|
||||||
platform_name Returns the name of the platform.
|
player_name Returns the name of the player.
|
||||||
total_plays Returns the play count for the platform.
|
platform_type Returns the name of the platform
|
||||||
|
total_plays Returns the play count for the player.
|
||||||
|
|
||||||
DOCUMENTATION :: END
|
DOCUMENTATION :: END
|
||||||
</%doc>
|
</%doc>
|
||||||
@@ -20,13 +21,13 @@ DOCUMENTATION :: END
|
|||||||
% if data != None:
|
% if data != None:
|
||||||
% for a in data:
|
% for a in data:
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<div class="user-platforms-instance">
|
<div class="user-player-instance">
|
||||||
<li>
|
<li>
|
||||||
<span id="user-platform-image-${a['result_id']}"></span>
|
<span id="user-player-image-${a['result_id']}"></span>
|
||||||
<div class="user-platforms-instance-name">
|
<div class="user-player-instance-name">
|
||||||
${a['platform_name']}
|
${a['player_name']}
|
||||||
</div>
|
</div>
|
||||||
<div class="user-platforms-instance-playcount">
|
<div class="user-player-instance-playcount">
|
||||||
<h3>${a['total_plays']}</h3>
|
<h3>${a['total_plays']}</h3>
|
||||||
<p> plays</p>
|
<p> plays</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +35,7 @@ DOCUMENTATION :: END
|
|||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
<script>
|
<script>
|
||||||
$("#user-platform-image-${a['result_id']}").html("<img class='user-platforms-instance-poster' src='" + getPlatformImagePath('${a['platform_type']}') + "'>");
|
$("#user-player-image-${a['result_id']}").html("<div class='user-player-instance-box' style='background-image: url(" + getPlatformImagePath('${a['platform_type']}') + ");'>");
|
||||||
</script>
|
</script>
|
||||||
% endfor
|
% endfor
|
||||||
% else:
|
% else:
|
||||||
@@ -18,6 +18,7 @@ time Returns the last watched time of the media.
|
|||||||
title Returns the name of the movie or episode.
|
title Returns the name of the movie or episode.
|
||||||
|
|
||||||
== Only if 'type' is 'episode ==
|
== Only if 'type' is 'episode ==
|
||||||
|
parent_title Returns the name of the TV Show a season belongs too.
|
||||||
parent_index Returns the season number.
|
parent_index Returns the season number.
|
||||||
index Returns the episode number.
|
index Returns the episode number.
|
||||||
|
|
||||||
@@ -31,28 +32,54 @@ DOCUMENTATION :: END
|
|||||||
<div class="dashboard-recent-media-row">
|
<div class="dashboard-recent-media-row">
|
||||||
<ul class="dashboard-recent-media list-unstyled">
|
<ul class="dashboard-recent-media list-unstyled">
|
||||||
% for item in data:
|
% for item in data:
|
||||||
<div class="dashboard-recent-media-instance">
|
|
||||||
<li>
|
<li>
|
||||||
<div class="poster">
|
% if item['type'] == 'episode' or item['type'] == 'movie':
|
||||||
<div class="poster-face">
|
|
||||||
<a href="info?source=history&item_id=${item['row_id']}">
|
<a href="info?source=history&item_id=${item['row_id']}">
|
||||||
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
|
<div class="dashboard-recent-media-poster">
|
||||||
</a>
|
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
|
||||||
|
<div class="dashboard-recent-media-overlay">
|
||||||
|
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||||
|
<script>
|
||||||
|
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-recent-media-metacontainer">
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
% if item['type'] == 'episode':
|
% if item['type'] == 'episode':
|
||||||
<h3>Season ${item['parentIndex']}, Episode ${item['index']}</h3>
|
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||||
|
<h3 title="${item['title']}">${item['title']}</h3>
|
||||||
|
<h3 class="text-muted">S${item['parent_index']} · E${item['index']}</h3>
|
||||||
% elif item['type'] == 'movie':
|
% elif item['type'] == 'movie':
|
||||||
<h3>${item['title']} (${item['year']})</h3>
|
<h3 title="${item['title']}">${item['title']}</h3>
|
||||||
|
<h3 class="text-muted">${item['year']}</h3>
|
||||||
% endif
|
% endif
|
||||||
<div class="text-muted" id="time-${item['time']}">${item['time']}</div>
|
<div class="text-muted" id="time-${item['time']}">
|
||||||
</div>
|
</div>
|
||||||
</li>
|
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
% elif item['type'] == 'track':
|
||||||
|
<a href="info?source=history&item_id=${item['row_id']}">
|
||||||
|
<div class="dashboard-recent-media-cover">
|
||||||
|
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
|
||||||
|
<div class="dashboard-recent-media-overlay">
|
||||||
|
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
|
||||||
<script>
|
<script>
|
||||||
$('#time-${item['time']}').html('Watched ' + moment(${item['time']}, "X").fromNow())
|
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
|
||||||
</script>
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
|
<h3 title="${item['grandparent_title']}">${item['grandparent_title']}</h3>
|
||||||
|
<h3 title="${item['title']}">${item['title']}</h3>
|
||||||
|
<h3 class="text-muted">${item['parent_title']}</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
% endif
|
||||||
|
</li>
|
||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,15 +30,16 @@ DOCUMENTATION :: END
|
|||||||
<h4>Last ${a['query_days']} days</h4>
|
<h4>Last ${a['query_days']} days</h4>
|
||||||
% endif
|
% endif
|
||||||
<h3>${a['total_plays']}</h3>
|
<h3>${a['total_plays']}</h3>
|
||||||
|
|
||||||
<p>plays</p>
|
<p>plays</p>
|
||||||
<span id="total-time-${a['query_days']}"></span>
|
<h3><strong>/</strong></h3>
|
||||||
</div>
|
<span id="total-time-${a['query_days']}">
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
<script>
|
<script>
|
||||||
$('#total-time-${a['query_days']}').html(humanTime(${a['total_time']}));
|
$('#total-time-${a['query_days']}').html(humanTime(${a['total_time']}));
|
||||||
</script>
|
</script>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
% endfor
|
% endfor
|
||||||
</ul>
|
</ul>
|
||||||
% else:
|
% else:
|
||||||
|
|||||||
@@ -9,26 +9,57 @@
|
|||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
<div class='table-card-header'>
|
<div class='table-card-header'>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<span><i class="fa fa-group"></i> Active Users</span>
|
<span><i class="fa fa-group"></i> All Users</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<button class="btn btn-dark" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
|
<button class="btn btn-dark refresh-users-button" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
|
||||||
|
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||||
|
<i class="fa fa-pencil"></i> Edit mode
|
||||||
|
</button> 
|
||||||
|
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i> Select users to delete/purge. Data is deleted/purged upon exiting edit mode.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='table-card-back'>
|
<div class='table-card-back'>
|
||||||
<table id="users_list_table" class="display" width="100%">
|
<table id="users_list_table" class="display" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th align="left" id="edit_row">Edit</th>
|
||||||
<th align="right" id="avatar"></th>
|
<th align="right" id="avatar"></th>
|
||||||
<th align="left" id="friendly_name">User</th>
|
<th align="left" id="friendly_name">User</th>
|
||||||
<th align="left" id="last_seen">Last Seen</th>
|
<th align="left" id="last_seen">Last Seen</th>
|
||||||
<th align="left" id="last_known_ip">Last Known IP</th>
|
<th align="left" id="last_known_ip">Last Known IP</th>
|
||||||
|
<th align="left" id="last_platform">Last Platform</th>
|
||||||
|
<th align="left" id="last_player">Last Player</th>
|
||||||
|
<th align="left" id="last_watched">Last Watched</th>
|
||||||
<th align="left" id="total_plays">Total Plays</th>
|
<th align="left" id="total_plays">Total Plays</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||||
|
<h4 class="modal-title" id="myModalLabel">Confirm Delete/Purge</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" style="text-align: center;">
|
||||||
|
<ul id="users-to-delete" class="list-unstyled"></ul>
|
||||||
|
<ul id="users-to-purge" class="list-unstyled"></ul>
|
||||||
|
<p>This is permanent and cannot be undone!</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,27 +72,116 @@
|
|||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
<script src="interfaces/default/js/tables/users.js"></script>
|
<script src="interfaces/default/js/tables/users.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
users_list_table_options.ajax = {
|
users_list_table_options.ajax = {
|
||||||
"url": "get_user_list",
|
url: 'get_user_list',
|
||||||
type: "post",
|
type: 'POST',
|
||||||
data: function ( d ) {
|
data: function ( d ) {
|
||||||
return { 'json_data': JSON.stringify( d ) };
|
return { 'json_data': JSON.stringify( d ) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var users_list_table = $('#users_list_table').DataTable(users_list_table_options);
|
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
|
||||||
|
|
||||||
|
clearSearchButton('users_list_table', users_list_table);
|
||||||
|
|
||||||
|
$('#row-edit-mode').on('click', function () {
|
||||||
|
$('#row-edit-mode-alert').fadeIn(200);
|
||||||
|
$('#users-to-delete').html('');
|
||||||
|
$('#users-to-purge').html('');
|
||||||
|
|
||||||
|
if ($(this).hasClass('active')) {
|
||||||
|
if (users_to_delete.length > 0 || users_to_purge.length > 0) {
|
||||||
|
$('.edit-control').each(function () {
|
||||||
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
users_to_purge = $.grep(users_to_purge, function (value) {
|
||||||
|
return $.inArray(value, users_to_delete) < 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users_to_delete.length > 0) {
|
||||||
|
$('#users-to-delete').prepend('<p>Are you REALLY sure you want to delete the following users:</p>')
|
||||||
|
for (var i = 0; i < users_to_delete.length; i++) {
|
||||||
|
$('#users-to-delete').append('<li>' + $('div[data-id=' + users_to_delete[i] + '] > input').val() + '</li>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users_to_purge.length > 0) {
|
||||||
|
$('#users-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following users:</p>')
|
||||||
|
for (var i = 0; i < users_to_purge.length; i++) {
|
||||||
|
$('#users-to-purge').append('<li>' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '</li>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#confirm-modal').modal();
|
||||||
|
$('#confirm-modal').one('click', '#confirm-delete', function () {
|
||||||
|
for (var i = 0; i < users_to_delete.length; i++) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_user',
|
||||||
|
data: { user_id: users_to_delete[i] },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "User deleted";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (var i = 0; i < users_to_purge.length; i++) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'delete_all_user_history',
|
||||||
|
data: { user_id: users_to_purge[i] },
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
success: function (data) {
|
||||||
|
var msg = "User history purged";
|
||||||
|
showMsg(msg, false, true, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
users_list_table.draw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.edit-control').each(function () {
|
||||||
|
$(this).addClass('hidden');
|
||||||
|
$('#row-edit-mode-alert').fadeOut(200);
|
||||||
|
});
|
||||||
|
$('.edit-user-control > .edit-user-name').each(function () {
|
||||||
|
a = $(this).children('a');
|
||||||
|
input = $(this).children('input');
|
||||||
|
a.text(input.val());
|
||||||
|
a.removeClass('hidden');
|
||||||
|
input.addClass('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
users_to_delete = [];
|
||||||
|
users_to_purge = [];
|
||||||
|
$('.edit-control').each(function () {
|
||||||
|
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||||
|
$(this).removeClass('hidden');
|
||||||
|
});
|
||||||
|
$('.edit-user-control > .edit-user-name').each(function () {
|
||||||
|
$(this).children('a').addClass('hidden');
|
||||||
|
$(this).children('input').removeClass('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$("#refresh-users-list").click(function() {
|
$("#refresh-users-list").click(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'refresh_users_list',
|
url: 'refresh_users_list',
|
||||||
cache: false,
|
cache: false,
|
||||||
async: true,
|
async: true,
|
||||||
success : function(data) {
|
success: function(data) {
|
||||||
showMsg('<i class="fa fa-check"></i> User list refresh started...',false,true,2000,false)
|
showMsg('<i class="fa fa-check"></i> User list refresh started...',false,true,2000,false)
|
||||||
},
|
},
|
||||||
error: function(jqXHR, textStatus, errorThrown) {
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh user list.',false,true,2000,true)
|
showMsg('<i class="fa fa-exclamation-circle"></i> Unable to refresh user list.',false,true,2000,true)
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<%
|
<%
|
||||||
import plexpy
|
import plexpy
|
||||||
from plexpy import version
|
from plexpy import common
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -15,6 +15,7 @@ from plexpy import version
|
|||||||
<link href="interfaces/default/css/bootstrap3/bootstrap.css" rel="stylesheet">
|
<link href="interfaces/default/css/bootstrap3/bootstrap.css" rel="stylesheet">
|
||||||
<link href="interfaces/default/css/bootstrap-wizard.css" rel="stylesheet">
|
<link href="interfaces/default/css/bootstrap-wizard.css" rel="stylesheet">
|
||||||
<link href="interfaces/default/css/plexpy.css" rel="stylesheet">
|
<link href="interfaces/default/css/plexpy.css" rel="stylesheet">
|
||||||
|
<link href="interfaces/default/css/selectize.bootstrap3.css" rel="stylesheet">
|
||||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
|
||||||
<link href="interfaces/default/css/font-awesome.min.css" rel="stylesheet">
|
<link href="interfaces/default/css/font-awesome.min.css" rel="stylesheet">
|
||||||
<link rel="icon" type="image/x-icon" href="interfaces/default/images/favicon.ico"/>
|
<link rel="icon" type="image/x-icon" href="interfaces/default/images/favicon.ico"/>
|
||||||
@@ -40,45 +41,6 @@ from plexpy import version
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-card" data-cardname="card2">
|
<div class="wizard-card" data-cardname="card2">
|
||||||
<h3>Plex Media Server</h3>
|
|
||||||
<form>
|
|
||||||
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure PlexPy can reach the server.</p>
|
|
||||||
<div class="wizard-input-section">
|
|
||||||
<label for="pms_ip">Plex IP or Hostname</label>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-8">
|
|
||||||
<input type="text" class="form-control pms-settings" name="pms_ip" id="pms_ip" placeholder="127.0.0.1" value="${config['pms_ip']}" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wizard-input-section">
|
|
||||||
<label for="pms_port">Port Number</label>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-3">
|
|
||||||
<input type="text" class="form-control pms-settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Force SSL
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-4">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1" ${config['pms_is_remote']}> Remote Server
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
|
|
||||||
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
|
||||||
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wizard-card" data-cardname="card3">
|
|
||||||
<h3>Plex Authentication</h3>
|
<h3>Plex Authentication</h3>
|
||||||
<p class="help-block">Enter your Plex.tv username and password. PlexPy does not store your username or password.</p>
|
<p class="help-block">Enter your Plex.tv username and password. PlexPy does not store your username or password.</p>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
@@ -100,11 +62,57 @@ from plexpy import version
|
|||||||
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken">
|
<input type="hidden" class="form-control pms-auth" name="pms_token" id="pms_token" value="${config['pms_token']}" data-validate="validatePMStoken">
|
||||||
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
<a class="btn btn-dark" id="pms-authenticate" href="#" role="button">Authenticate</a><span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="wizard-card" data-cardname="card3">
|
||||||
|
<h3>Plex Media Server</h3>
|
||||||
|
<form>
|
||||||
|
<p class="help-block">Enter your Plex Server details and then click the Verify button to make sure PlexPy can reach the server.</p>
|
||||||
|
<div class="wizard-input-section">
|
||||||
|
<label for="pms_ip">Plex IP or Hostname</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-8">
|
||||||
|
<select id="pms_ip" name="pms_ip"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wizard-input-section">
|
||||||
|
<label for="pms_port">Port Number</label>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-3">
|
||||||
|
<input type="text" class="form-control pms_settings" name="pms_port" id="pms_port" placeholder="32400" value="${config['pms_port']}" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Force SSL
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1"> Remote Server
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" class="form-control pms-settings" id="pms_valid" data-validate="validatePMSip" value="">
|
||||||
|
<input type="hidden" class="form-control pms-settings" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||||
|
<a class="btn btn-dark" id="verify-plex-server" href="#" role="button">Verify</a><span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="wizard-card" data-cardname="card4">
|
<div class="wizard-card" data-cardname="card4">
|
||||||
<h3>Monitoring</h3>
|
<h3>Monitoring</h3>
|
||||||
|
<p class="help-block">Keep records of all movie, TV show, or music items played from your Plex Media Server.</p>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<input type="checkbox" id="video_logging_enable" name="video_logging_enable" value="1" ${config['video_logging_enable']}> Log Movies and TV
|
<input type="checkbox" id="movie_logging_enable" name="movie_logging_enable" value="1" ${config['movie_logging_enable']}> Enable Movie Logging
|
||||||
<p class="help-block">Keep records of all video items played from your Plex Media Server.</p>
|
</div>
|
||||||
|
<div class="wizard-input-section">
|
||||||
|
<input type="checkbox" id="tv_logging_enable" name="tv_logging_enable" value="1" ${config['tv_logging_enable']}> Enable TV Show Logging
|
||||||
|
</div>
|
||||||
|
<div class="wizard-input-section">
|
||||||
|
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Enable Music Logging
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<label for="logging_ignore_interval">Ignore Interval</label>
|
<label for="logging_ignore_interval">Ignore Interval</label>
|
||||||
@@ -116,19 +124,16 @@ from plexpy import version
|
|||||||
</div>
|
</div>
|
||||||
<p class="help-block">The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.</p>
|
<p class="help-block">The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Music logging is still very experimental -- leave this for now.
|
|
||||||
<div class="wizard-input-section">
|
|
||||||
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1"> Log Music
|
|
||||||
<p class="help-block">Keep records of all audio items played from your Plex Media Server. VERY experimental.</p>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-card" data-cardname="card5" data-validate="validateNotifications">
|
<div class="wizard-card" data-cardname="card5" data-validate="validateNotifications">
|
||||||
<h3>Notifications</h3>
|
<h3>Notifications</h3>
|
||||||
<p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent conifgure this in <strong>Settings -> Notification Agents</strong>
|
<p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent conifgure this in <strong>Settings -> Notification Agents</strong>
|
||||||
after you have completed this setup wizard.</p><br/>
|
after you have completed this setup wizard.</p><br/>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie and TV playback
|
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie playback
|
||||||
|
</div>
|
||||||
|
<div class="wizard-input-section">
|
||||||
|
<input type="checkbox" name="tv_notify_enable" id="tv_notify_enable" value="1" ${config['tv_notify_enable']}> Enable notifications on TV Show playback
|
||||||
</div>
|
</div>
|
||||||
<div class="wizard-input-section">
|
<div class="wizard-input-section">
|
||||||
<input type="checkbox" name="music_notify_enable" id="music_notify_enable" value="1" ${config['music_notify_enable']}> Enable notifications on Music playback
|
<input type="checkbox" name="music_notify_enable" id="music_notify_enable" value="1" ${config['music_notify_enable']}> Enable notifications on Music playback
|
||||||
@@ -184,6 +189,7 @@ from plexpy import version
|
|||||||
|
|
||||||
<script src="interfaces/default/js/jquery-2.1.4.min.js"></script>
|
<script src="interfaces/default/js/jquery-2.1.4.min.js"></script>
|
||||||
<script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script>
|
<script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script>
|
||||||
|
<script src="interfaces/default/js/selectize.min.js"></script>
|
||||||
<script src="interfaces/default/js/script.js"></script>
|
<script src="interfaces/default/js/script.js"></script>
|
||||||
<script src="interfaces/default/js/bootstrap-wizard.min.js"></script>
|
<script src="interfaces/default/js/bootstrap-wizard.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -218,6 +224,74 @@ from plexpy import version
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$select_pms = $('#pms_ip').selectize({
|
||||||
|
create: true,
|
||||||
|
createOnBlur: true,
|
||||||
|
openOnFocus: true,
|
||||||
|
maxItems: 1,
|
||||||
|
closeAfterSelect: true,
|
||||||
|
onInitialize: function () {
|
||||||
|
var s = this;
|
||||||
|
this.revertSettings.$children.each(function () {
|
||||||
|
$.extend(s.options[this.value], $(this).data());
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
render: {
|
||||||
|
option: function (item, escape) {
|
||||||
|
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
|
||||||
|
},
|
||||||
|
item: function (item, escape) {
|
||||||
|
// first item is rendered before initialization bug?
|
||||||
|
if (!item.ci) {
|
||||||
|
$.extend(item,
|
||||||
|
$(this.revertSettings.$children)
|
||||||
|
.filter('[value="' + item.value + '"]').data());
|
||||||
|
|
||||||
|
}
|
||||||
|
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange: function (item) {
|
||||||
|
var ci = $('.selectize-input').find('div').attr('data-ci');
|
||||||
|
var port = $('.selectize-input').find('div').attr('data-port')
|
||||||
|
var local = $('.selectize-input').find('div').attr('data-local')
|
||||||
|
var ssl = $('.selectize-input').find('div').attr('data-use_ssl')
|
||||||
|
|
||||||
|
$("#pms-verify-status").html("");
|
||||||
|
// If a option was added by a user its
|
||||||
|
// data-xxx="undefined"
|
||||||
|
if (ci != "undefined") {
|
||||||
|
// To allow next step in the guide.
|
||||||
|
// servers with clientIdentifier is verified
|
||||||
|
$("#pms_identifier").val(ci);
|
||||||
|
$("#pms_valid").val("valid");
|
||||||
|
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
|
||||||
|
} else {
|
||||||
|
// Self made options must be verified
|
||||||
|
$("#pms_valid").val("");
|
||||||
|
$("#pms-verify-status").html("").hide();
|
||||||
|
}
|
||||||
|
// If the server is verified set the correct port
|
||||||
|
if (port != "undefined") {
|
||||||
|
$('#pms_port').val(port);
|
||||||
|
} else {
|
||||||
|
// set default port
|
||||||
|
$('#pms_port').val("32400");
|
||||||
|
}
|
||||||
|
if (local != "undefined" && local == '0') {
|
||||||
|
$('#pms_is_remote').prop('checked', true);
|
||||||
|
} else {
|
||||||
|
$('#pms_is_remote').prop('checked', false);
|
||||||
|
}
|
||||||
|
if (ssl != "undefined" && ssl == "1") {
|
||||||
|
$('#pms_ssl').prop('checked', true);
|
||||||
|
} else {
|
||||||
|
$('#pms_ssl').prop('checked', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -355,7 +429,9 @@ from plexpy import version
|
|||||||
headers: {'Content-Type': 'application/xml; charset=utf-8',
|
headers: {'Content-Type': 'application/xml; charset=utf-8',
|
||||||
'X-Plex-Device-Name': 'PlexPy',
|
'X-Plex-Device-Name': 'PlexPy',
|
||||||
'X-Plex-Product': 'PlexPy',
|
'X-Plex-Product': 'PlexPy',
|
||||||
'X-Plex-Version': 'v0.1 dev',
|
'X-Plex-Version': '${common.VERSION_NUMBER}',
|
||||||
|
'X-Plex-Platform': '${common.PLATFORM}',
|
||||||
|
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
|
||||||
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
|
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
|
||||||
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
|
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
|
||||||
},
|
},
|
||||||
@@ -369,6 +445,7 @@ from plexpy import version
|
|||||||
$('#pms-token-status').fadeIn('fast');
|
$('#pms-token-status').fadeIn('fast');
|
||||||
$("#pms_token").val(authToken);
|
$("#pms_token").val(authToken);
|
||||||
authenticated = true;
|
authenticated = true;
|
||||||
|
getServerOptions(authToken)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -397,6 +474,18 @@ from plexpy import version
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getServerOptions(token) {
|
||||||
|
/* Set token and returns server options */
|
||||||
|
$.ajax({
|
||||||
|
url: "discover/" + token,
|
||||||
|
success: function (result) {
|
||||||
|
$('#pms_ip').html("")
|
||||||
|
// Add all servers to the "combobox"
|
||||||
|
$select_pms[0].selectize.addOption(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# PlexPy - Automatic music downloader for SABnzbd
|
# PlexPy - Stats for Plex Media Server usage
|
||||||
#
|
#
|
||||||
# Service Unit file for systemd system manager
|
# Service Unit file for systemd system manager
|
||||||
#
|
#
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=PlexPy - Automatic music downloader for SABnzbd
|
Description=PlexPy - Stats for Plex Media Server usage
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet
|
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet
|
||||||
|
|||||||
80
init-scripts/init.freenas
Normal file
80
init-scripts/init.freenas
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PROVIDE: plexpy
|
||||||
|
# REQUIRE: DAEMON sabnzbd
|
||||||
|
# KEYWORD: shutdown
|
||||||
|
#
|
||||||
|
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||||
|
# to enable this service:
|
||||||
|
#
|
||||||
|
# plexpy_enable (bool): Set to NO by default.
|
||||||
|
# Set it to YES to enable it.
|
||||||
|
# plexpy_user: The user account PlexPy daemon runs as what
|
||||||
|
# you want it to be. It uses '_sabnzbd' user by
|
||||||
|
# default. Do not sets it as empty or it will run
|
||||||
|
# as root.
|
||||||
|
# plexpy_dir: Directory where PlexPy lives.
|
||||||
|
# Default: /usr/local/plexpy
|
||||||
|
# plexpy_chdir: Change to this directory before running PlexPy.
|
||||||
|
# Default is same as plexpy_dir.
|
||||||
|
# plexpy_pid: The name of the pidfile to create.
|
||||||
|
# Default is plexpy.pid in plexpy_dir.
|
||||||
|
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||||
|
|
||||||
|
. /etc/rc.subr
|
||||||
|
|
||||||
|
name="plexpy"
|
||||||
|
rcvar=${name}_enable
|
||||||
|
|
||||||
|
load_rc_config ${name}
|
||||||
|
|
||||||
|
: ${plexpy_enable:="NO"}
|
||||||
|
: ${plexpy_user:="_sabnzbd"}
|
||||||
|
: ${plexpy_dir:="/usr/local/share/plexpy"}
|
||||||
|
: ${plexpy_chdir:="${plexpy_dir}"}
|
||||||
|
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
|
||||||
|
|
||||||
|
status_cmd="${name}_status"
|
||||||
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
|
command="/usr/sbin/daemon"
|
||||||
|
command_args="-f -p ${plexpy_pid} python2 ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
|
||||||
|
|
||||||
|
# Ensure user is root when running this script.
|
||||||
|
if [ `id -u` != "0" ]; then
|
||||||
|
echo "Oops, you should be root before running this!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify_plexpy_pid() {
|
||||||
|
# Make sure the pid corresponds to the PlexPy process.
|
||||||
|
if [ -f ${plexpy_pid} ]; then
|
||||||
|
pid=`cat ${plexpy_pid} 2>/dev/null`
|
||||||
|
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
|
||||||
|
return $?
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to stop PlexPy cleanly by calling shutdown over http.
|
||||||
|
plexpy_stop() {
|
||||||
|
echo "Stopping $name."
|
||||||
|
verify_plexpy_pid
|
||||||
|
if [ -n "${pid}" ]; then
|
||||||
|
kill ${pid}
|
||||||
|
wait_for_pids ${pid}
|
||||||
|
echo "Stopped."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
plexpy_status() {
|
||||||
|
verify_plexpy_pid
|
||||||
|
if [ -n "${pid}" ]; then
|
||||||
|
echo "$name is running as ${pid}."
|
||||||
|
else
|
||||||
|
echo "$name is not running."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_rc_command "$1"
|
||||||
67
init-scripts/init.opensuse.systemd
Normal file
67
init-scripts/init.opensuse.systemd
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# PlexPy - Stats for Plex Media Server usage
|
||||||
|
#
|
||||||
|
# Service Unit file for systemd system manager
|
||||||
|
#
|
||||||
|
# INSTALLATION NOTES
|
||||||
|
#
|
||||||
|
# 1. Rename this file as you want, ensuring that it ends in .service
|
||||||
|
# e.g. 'plexpy.service'
|
||||||
|
#
|
||||||
|
# 2. Adjust configuration settings as required. More details in the
|
||||||
|
# "CONFIGURATION NOTES" section shown below.
|
||||||
|
#
|
||||||
|
# 3. Copy this file into your systemd service unit directory, which is
|
||||||
|
# often '/lib/systemd/system'.
|
||||||
|
#
|
||||||
|
# 4. Create any files/directories that you specified back in step #2.
|
||||||
|
# e.g. '/opt/plexpy.ini'
|
||||||
|
# '/opt/plexpy'
|
||||||
|
#
|
||||||
|
# 5. Enable boot-time autostart with the following commands:
|
||||||
|
# systemctl daemon-reload
|
||||||
|
# systemctl enable plexpy.service
|
||||||
|
#
|
||||||
|
# 6. Start now with the following command:
|
||||||
|
# systemctl start plexpy.service
|
||||||
|
#
|
||||||
|
# 7. If troubleshooting startup-errors, start by checking permissions
|
||||||
|
# and ownership on the files/directories that you created in step #4.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# CONFIGURATION NOTES
|
||||||
|
#
|
||||||
|
# - The example settings in this file assume that:
|
||||||
|
# 1. You will run PlexPy as user/group: plex.users
|
||||||
|
# 2. You will either have PlexPy installed as a subdirectory
|
||||||
|
# under '/opt', or that you will have a symlink under
|
||||||
|
# '/opt' pointing to your PlexPy install dir.
|
||||||
|
# 3. Your PlexPy data directory and configuration file can be
|
||||||
|
# in separate locations from your PlexPy install dir, to
|
||||||
|
# simplify updates. However, in the example below they are in the
|
||||||
|
# PlexPy install dir.
|
||||||
|
#
|
||||||
|
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
|
||||||
|
#
|
||||||
|
# - Adjust ExecStart= to point to:
|
||||||
|
# 1. Your PlexPy executable,
|
||||||
|
# 2. Your config file (recommended is to put it somewhere in /etc)
|
||||||
|
# 3. Your datadir (recommended is to NOT put it in your PlexPy exec dir)
|
||||||
|
#
|
||||||
|
# - Adjust User= and Group= to the user/group you want PlexPy to run as.
|
||||||
|
#
|
||||||
|
# - WantedBy= specifies which target (i.e. runlevel) to start PlexPy for.
|
||||||
|
# multi-user.target equates to runlevel 3 (multi-user text mode)
|
||||||
|
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=PlexPy - Stats for Plex Media Server usage
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/opt/plexpy/PlexPy.py --daemon --config /opt/plexpy/config.ini --datadir /opt/plexpy --nolaunch --quiet
|
||||||
|
GuessMainPID=no
|
||||||
|
Type=forking
|
||||||
|
User=plex
|
||||||
|
Group=users
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
66
init-scripts/init.ubuntu.systemd
Normal file
66
init-scripts/init.ubuntu.systemd
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# PlexPy - Stats for Plex Media Server usage
|
||||||
|
#
|
||||||
|
# Service Unit file for systemd system manager
|
||||||
|
#
|
||||||
|
# INSTALLATION NOTES
|
||||||
|
#
|
||||||
|
# 1. Rename this file as you want, ensuring that it ends in .service
|
||||||
|
# e.g. 'plexpy.service'
|
||||||
|
#
|
||||||
|
# 2. Adjust configuration settings as required. More details in the
|
||||||
|
# "CONFIGURATION NOTES" section shown below.
|
||||||
|
#
|
||||||
|
# 3. Copy this file into your systemd service unit directory, which is
|
||||||
|
# often '/lib/systemd/system'.
|
||||||
|
#
|
||||||
|
# 4. Create any files/directories that you specified back in step #2.
|
||||||
|
# e.g. '/etc/plexpy/plexpy.ini'
|
||||||
|
# '/home/sabnzbd/.plexpy'
|
||||||
|
#
|
||||||
|
# 5. Enable boot-time autostart with the following commands:
|
||||||
|
# systemctl daemon-reload
|
||||||
|
# systemctl enable plexpy.service
|
||||||
|
#
|
||||||
|
# 6. Start now with the following command:
|
||||||
|
# systemctl start plexpy.service
|
||||||
|
#
|
||||||
|
# 7. If troubleshooting startup-errors, start by checking permissions
|
||||||
|
# and ownership on the files/directories that you created in step #4.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# CONFIGURATION NOTES
|
||||||
|
#
|
||||||
|
# - The example settings in this file assume that:
|
||||||
|
# 1. You will run PlexPy as user/group: sabnzbd.sabnzbd
|
||||||
|
# 2. You will either have PlexPy installed as a subdirectory
|
||||||
|
# under '~sabnzbd', or that you will have a symlink under
|
||||||
|
# '~/sabnzbd' pointing to your PlexPy install dir.
|
||||||
|
# 3. Your PlexPy data directory and configuration file will be
|
||||||
|
# in separate locations from your PlexPy install dir, to
|
||||||
|
# simplify updates.
|
||||||
|
#
|
||||||
|
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
|
||||||
|
#
|
||||||
|
# - Adjust ExecStart= to point to:
|
||||||
|
# 1. Your PlexPy executable,
|
||||||
|
# 2. Your config file (recommended is to put it somewhere in /etc)
|
||||||
|
# 3. Your datadir (recommended is to NOT put it in your PlexPy exec dir)
|
||||||
|
#
|
||||||
|
# - Adjust User= and Group= to the user/group you want PlexPy to run as.
|
||||||
|
#
|
||||||
|
# - WantedBy= specifies which target (i.e. runlevel) to start PlexPy for.
|
||||||
|
# multi-user.target equates to runlevel 3 (multi-user text mode)
|
||||||
|
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=PlexPy - Stats for Plex Media Server usage
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/opt/plexpy/PlexPy.py --quiet --daemon --nolaunch --config /opt/plexpy/config.ini --datadir /opt/plexpy
|
||||||
|
GuessMainPID=no
|
||||||
|
Type=forking
|
||||||
|
User=plexpy
|
||||||
|
Group=nogroup
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# plexpy - Automatic music downloader
|
# plexpy
|
||||||
#
|
#
|
||||||
# This is a session/user job. Install this file into /usr/share/upstart/sessions
|
# This is a session/user job. Install this file into /usr/share/upstart/sessions
|
||||||
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if
|
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if
|
||||||
|
|||||||
25
lib/websocket/__init__.py
Normal file
25
lib/websocket/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
from ._core import *
|
||||||
|
from ._app import WebSocketApp
|
||||||
|
|
||||||
|
__version__ = "0.32.0"
|
||||||
382
lib/websocket/_abnf.py
Normal file
382
lib/websocket/_abnf.py
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
import six
|
||||||
|
import array
|
||||||
|
import struct
|
||||||
|
import os
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._utils import validate_utf8
|
||||||
|
|
||||||
|
# closing frame status codes.
|
||||||
|
STATUS_NORMAL = 1000
|
||||||
|
STATUS_GOING_AWAY = 1001
|
||||||
|
STATUS_PROTOCOL_ERROR = 1002
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
||||||
|
STATUS_STATUS_NOT_AVAILABLE = 1005
|
||||||
|
STATUS_ABNORMAL_CLOSED = 1006
|
||||||
|
STATUS_INVALID_PAYLOAD = 1007
|
||||||
|
STATUS_POLICY_VIOLATION = 1008
|
||||||
|
STATUS_MESSAGE_TOO_BIG = 1009
|
||||||
|
STATUS_INVALID_EXTENSION = 1010
|
||||||
|
STATUS_UNEXPECTED_CONDITION = 1011
|
||||||
|
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
||||||
|
|
||||||
|
VALID_CLOSE_STATUS = (
|
||||||
|
STATUS_NORMAL,
|
||||||
|
STATUS_GOING_AWAY,
|
||||||
|
STATUS_PROTOCOL_ERROR,
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE,
|
||||||
|
STATUS_INVALID_PAYLOAD,
|
||||||
|
STATUS_POLICY_VIOLATION,
|
||||||
|
STATUS_MESSAGE_TOO_BIG,
|
||||||
|
STATUS_INVALID_EXTENSION,
|
||||||
|
STATUS_UNEXPECTED_CONDITION,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ABNF(object):
|
||||||
|
"""
|
||||||
|
ABNF frame class.
|
||||||
|
see http://tools.ietf.org/html/rfc5234
|
||||||
|
and http://tools.ietf.org/html/rfc6455#section-5.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
# operation code values.
|
||||||
|
OPCODE_CONT = 0x0
|
||||||
|
OPCODE_TEXT = 0x1
|
||||||
|
OPCODE_BINARY = 0x2
|
||||||
|
OPCODE_CLOSE = 0x8
|
||||||
|
OPCODE_PING = 0x9
|
||||||
|
OPCODE_PONG = 0xa
|
||||||
|
|
||||||
|
# available operation code value tuple
|
||||||
|
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
||||||
|
OPCODE_PING, OPCODE_PONG)
|
||||||
|
|
||||||
|
# opcode human readable string
|
||||||
|
OPCODE_MAP = {
|
||||||
|
OPCODE_CONT: "cont",
|
||||||
|
OPCODE_TEXT: "text",
|
||||||
|
OPCODE_BINARY: "binary",
|
||||||
|
OPCODE_CLOSE: "close",
|
||||||
|
OPCODE_PING: "ping",
|
||||||
|
OPCODE_PONG: "pong"
|
||||||
|
}
|
||||||
|
|
||||||
|
# data length threashold.
|
||||||
|
LENGTH_7 = 0x7e
|
||||||
|
LENGTH_16 = 1 << 16
|
||||||
|
LENGTH_63 = 1 << 63
|
||||||
|
|
||||||
|
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
|
||||||
|
opcode=OPCODE_TEXT, mask=1, data=""):
|
||||||
|
"""
|
||||||
|
Constructor for ABNF.
|
||||||
|
please check RFC for arguments.
|
||||||
|
"""
|
||||||
|
self.fin = fin
|
||||||
|
self.rsv1 = rsv1
|
||||||
|
self.rsv2 = rsv2
|
||||||
|
self.rsv3 = rsv3
|
||||||
|
self.opcode = opcode
|
||||||
|
self.mask = mask
|
||||||
|
if data == None:
|
||||||
|
data = ""
|
||||||
|
self.data = data
|
||||||
|
self.get_mask_key = os.urandom
|
||||||
|
|
||||||
|
def validate(self, skip_utf8_validation=False):
|
||||||
|
"""
|
||||||
|
validate the ABNF frame.
|
||||||
|
skip_utf8_validation: skip utf8 validation.
|
||||||
|
"""
|
||||||
|
if self.rsv1 or self.rsv2 or self.rsv3:
|
||||||
|
raise WebSocketProtocolException("rsv is not implemented, yet")
|
||||||
|
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_PING and not self.fin:
|
||||||
|
raise WebSocketProtocolException("Invalid ping frame.")
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
l = len(self.data)
|
||||||
|
if not l:
|
||||||
|
return
|
||||||
|
if l == 1 or l >= 126:
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
|
||||||
|
code = 256*six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2])
|
||||||
|
if not self._is_valid_close_status(code):
|
||||||
|
raise WebSocketProtocolException("Invalid close opcode.")
|
||||||
|
|
||||||
|
def _is_valid_close_status(self, code):
|
||||||
|
return code in VALID_CLOSE_STATUS or (3000 <= code <5000)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "fin=" + str(self.fin) \
|
||||||
|
+ " opcode=" + str(self.opcode) \
|
||||||
|
+ " data=" + str(self.data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_frame(data, opcode, fin=1):
|
||||||
|
"""
|
||||||
|
create frame to send text, binary and other data.
|
||||||
|
|
||||||
|
data: data to send. This is string value(byte array).
|
||||||
|
if opcode is OPCODE_TEXT and this value is uniocde,
|
||||||
|
data value is conveted into unicode string, automatically.
|
||||||
|
|
||||||
|
opcode: operation code. please see OPCODE_XXX.
|
||||||
|
|
||||||
|
fin: fin flag. if set to 0, create continue fragmentation.
|
||||||
|
"""
|
||||||
|
if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type):
|
||||||
|
data = data.encode("utf-8")
|
||||||
|
# mask must be set if send data from client
|
||||||
|
return ABNF(fin, 0, 0, 0, opcode, 1, data)
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
"""
|
||||||
|
format this object to string(byte array) to send data to server.
|
||||||
|
"""
|
||||||
|
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
||||||
|
raise ValueError("not 0 or 1")
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise ValueError("Invalid OPCODE")
|
||||||
|
length = len(self.data)
|
||||||
|
if length >= ABNF.LENGTH_63:
|
||||||
|
raise ValueError("data is too long")
|
||||||
|
|
||||||
|
frame_header = chr(self.fin << 7
|
||||||
|
| self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
|
||||||
|
| self.opcode)
|
||||||
|
if length < ABNF.LENGTH_7:
|
||||||
|
frame_header += chr(self.mask << 7 | length)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
elif length < ABNF.LENGTH_16:
|
||||||
|
frame_header += chr(self.mask << 7 | 0x7e)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
frame_header += struct.pack("!H", length)
|
||||||
|
else:
|
||||||
|
frame_header += chr(self.mask << 7 | 0x7f)
|
||||||
|
frame_header = six.b(frame_header)
|
||||||
|
frame_header += struct.pack("!Q", length)
|
||||||
|
|
||||||
|
if not self.mask:
|
||||||
|
return frame_header + self.data
|
||||||
|
else:
|
||||||
|
mask_key = self.get_mask_key(4)
|
||||||
|
return frame_header + self._get_masked(mask_key)
|
||||||
|
|
||||||
|
def _get_masked(self, mask_key):
|
||||||
|
s = ABNF.mask(mask_key, self.data)
|
||||||
|
|
||||||
|
if isinstance(mask_key, six.text_type):
|
||||||
|
mask_key = mask_key.encode('utf-8')
|
||||||
|
|
||||||
|
return mask_key + s
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mask(mask_key, data):
|
||||||
|
"""
|
||||||
|
mask or unmask data. Just do xor for each byte
|
||||||
|
|
||||||
|
mask_key: 4 byte string(byte).
|
||||||
|
|
||||||
|
data: data to mask/unmask.
|
||||||
|
"""
|
||||||
|
if data == None:
|
||||||
|
data = ""
|
||||||
|
if isinstance(mask_key, six.text_type):
|
||||||
|
mask_key = six.b(mask_key)
|
||||||
|
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = six.b(data)
|
||||||
|
|
||||||
|
_m = array.array("B", mask_key)
|
||||||
|
_d = array.array("B", data)
|
||||||
|
for i in range(len(_d)):
|
||||||
|
_d[i] ^= _m[i % 4]
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
return _d.tobytes()
|
||||||
|
else:
|
||||||
|
return _d.tostring()
|
||||||
|
|
||||||
|
|
||||||
|
class frame_buffer(object):
|
||||||
|
_HEADER_MASK_INDEX = 5
|
||||||
|
_HEADER_LENGHT_INDEX = 6
|
||||||
|
|
||||||
|
def __init__(self, recv_fn, skip_utf8_validation):
|
||||||
|
self.recv = recv_fn
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
# Buffers over the packets from the layer beneath until desired amount
|
||||||
|
# bytes of bytes are received.
|
||||||
|
self.recv_buffer = []
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.header = None
|
||||||
|
self.length = None
|
||||||
|
self.mask = None
|
||||||
|
|
||||||
|
def has_received_header(self):
|
||||||
|
return self.header is None
|
||||||
|
|
||||||
|
def recv_header(self):
|
||||||
|
header = self.recv_strict(2)
|
||||||
|
b1 = header[0]
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
b1 = ord(b1)
|
||||||
|
|
||||||
|
fin = b1 >> 7 & 1
|
||||||
|
rsv1 = b1 >> 6 & 1
|
||||||
|
rsv2 = b1 >> 5 & 1
|
||||||
|
rsv3 = b1 >> 4 & 1
|
||||||
|
opcode = b1 & 0xf
|
||||||
|
b2 = header[1]
|
||||||
|
|
||||||
|
if six.PY2:
|
||||||
|
b2 = ord(b2)
|
||||||
|
|
||||||
|
has_mask = b2 >> 7 & 1
|
||||||
|
length_bits = b2 & 0x7f
|
||||||
|
|
||||||
|
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
|
||||||
|
|
||||||
|
def has_mask(self):
|
||||||
|
if not self.header:
|
||||||
|
return False
|
||||||
|
return self.header[frame_buffer._HEADER_MASK_INDEX]
|
||||||
|
|
||||||
|
|
||||||
|
def has_received_length(self):
|
||||||
|
return self.length is None
|
||||||
|
|
||||||
|
def recv_length(self):
|
||||||
|
bits = self.header[frame_buffer._HEADER_LENGHT_INDEX]
|
||||||
|
length_bits = bits & 0x7f
|
||||||
|
if length_bits == 0x7e:
|
||||||
|
v = self.recv_strict(2)
|
||||||
|
self.length = struct.unpack("!H", v)[0]
|
||||||
|
elif length_bits == 0x7f:
|
||||||
|
v = self.recv_strict(8)
|
||||||
|
self.length = struct.unpack("!Q", v)[0]
|
||||||
|
else:
|
||||||
|
self.length = length_bits
|
||||||
|
|
||||||
|
def has_received_mask(self):
|
||||||
|
return self.mask is None
|
||||||
|
|
||||||
|
def recv_mask(self):
|
||||||
|
self.mask = self.recv_strict(4) if self.has_mask() else ""
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
# Header
|
||||||
|
if self.has_received_header():
|
||||||
|
self.recv_header()
|
||||||
|
(fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
|
||||||
|
|
||||||
|
# Frame length
|
||||||
|
if self.has_received_length():
|
||||||
|
self.recv_length()
|
||||||
|
length = self.length
|
||||||
|
|
||||||
|
# Mask
|
||||||
|
if self.has_received_mask():
|
||||||
|
self.recv_mask()
|
||||||
|
mask = self.mask
|
||||||
|
|
||||||
|
# Payload
|
||||||
|
payload = self.recv_strict(length)
|
||||||
|
if has_mask:
|
||||||
|
payload = ABNF.mask(mask, payload)
|
||||||
|
|
||||||
|
# Reset for next frame
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
||||||
|
frame.validate(self.skip_utf8_validation)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def recv_strict(self, bufsize):
|
||||||
|
shortage = bufsize - sum(len(x) for x in self.recv_buffer)
|
||||||
|
while shortage > 0:
|
||||||
|
# Limit buffer size that we pass to socket.recv() to avoid
|
||||||
|
# fragmenting the heap -- the number of bytes recv() actually
|
||||||
|
# reads is limited by socket buffer and is relatively small,
|
||||||
|
# yet passing large numbers repeatedly causes lots of large
|
||||||
|
# buffers allocated and then shrunk, which results in fragmentation.
|
||||||
|
bytes = self.recv(min(16384, shortage))
|
||||||
|
self.recv_buffer.append(bytes)
|
||||||
|
shortage -= len(bytes)
|
||||||
|
|
||||||
|
unified = six.b("").join(self.recv_buffer)
|
||||||
|
|
||||||
|
if shortage == 0:
|
||||||
|
self.recv_buffer = []
|
||||||
|
return unified
|
||||||
|
else:
|
||||||
|
self.recv_buffer = [unified[bufsize:]]
|
||||||
|
return unified[:bufsize]
|
||||||
|
|
||||||
|
|
||||||
|
class continuous_frame(object):
|
||||||
|
def __init__(self, fire_cont_frame, skip_utf8_validation):
|
||||||
|
self.fire_cont_frame = fire_cont_frame
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
self.cont_data = None
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def validate(self, frame):
|
||||||
|
if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
if self.recving_frames and frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
|
||||||
|
def add(self, frame):
|
||||||
|
if self.cont_data:
|
||||||
|
self.cont_data[1] += frame.data
|
||||||
|
else:
|
||||||
|
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
self.recving_frames = frame.opcode
|
||||||
|
self.cont_data = [frame.opcode, frame.data]
|
||||||
|
|
||||||
|
if frame.fin:
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def is_fire(self, frame):
|
||||||
|
return frame.fin or self.fire_cont_frame
|
||||||
|
|
||||||
|
def extract(self, frame):
|
||||||
|
data = self.cont_data
|
||||||
|
self.cont_data = None
|
||||||
|
frame.data = data[1]
|
||||||
|
if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data):
|
||||||
|
raise WebSocketPayloadException("cannot decode: " + repr(frame.data))
|
||||||
|
|
||||||
|
return [data[0], frame]
|
||||||
236
lib/websocket/_app.py
Normal file
236
lib/websocket/_app.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
WebSocketApp provides higher level APIs.
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
import select
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._core import WebSocket, getdefaulttimeout
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._logging import *
|
||||||
|
from websocket._abnf import ABNF
|
||||||
|
|
||||||
|
__all__ = ["WebSocketApp"]
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketApp(object):
|
||||||
|
"""
|
||||||
|
Higher level of APIs are provided.
|
||||||
|
The interface is like JavaScript WebSocket object.
|
||||||
|
"""
|
||||||
|
def __init__(self, url, header=[],
|
||||||
|
on_open=None, on_message=None, on_error=None,
|
||||||
|
on_close=None, on_ping=None, on_pong=None,
|
||||||
|
on_cont_message=None,
|
||||||
|
keep_running=True, get_mask_key=None, cookie=None,
|
||||||
|
subprotocols=None):
|
||||||
|
"""
|
||||||
|
url: websocket url.
|
||||||
|
header: custom header for websocket handshake.
|
||||||
|
on_open: callable object which is called at opening websocket.
|
||||||
|
this function has one argument. The arugment is this class object.
|
||||||
|
on_message: callbale object which is called when recieved data.
|
||||||
|
on_message has 2 arguments.
|
||||||
|
The 1st arugment is this class object.
|
||||||
|
The passing 2nd arugment is utf-8 string which we get from the server.
|
||||||
|
on_error: callable object which is called when we get error.
|
||||||
|
on_error has 2 arguments.
|
||||||
|
The 1st arugment is this class object.
|
||||||
|
The passing 2nd arugment is exception object.
|
||||||
|
on_close: callable object which is called when closed the connection.
|
||||||
|
this function has one argument. The arugment is this class object.
|
||||||
|
on_cont_message: callback object which is called when recieve continued
|
||||||
|
frame data.
|
||||||
|
on_message has 3 arguments.
|
||||||
|
The 1st arugment is this class object.
|
||||||
|
The passing 2nd arugment is utf-8 string which we get from the server.
|
||||||
|
The 3rd arugment is continue flag. if 0, the data continue
|
||||||
|
to next frame data
|
||||||
|
keep_running: a boolean flag indicating whether the app's main loop
|
||||||
|
should keep running, defaults to True
|
||||||
|
get_mask_key: a callable to produce new mask keys,
|
||||||
|
see the WebSocket.set_mask_key's docstring for more information
|
||||||
|
subprotocols: array of available sub protocols. default is None.
|
||||||
|
"""
|
||||||
|
self.url = url
|
||||||
|
self.header = header
|
||||||
|
self.cookie = cookie
|
||||||
|
self.on_open = on_open
|
||||||
|
self.on_message = on_message
|
||||||
|
self.on_error = on_error
|
||||||
|
self.on_close = on_close
|
||||||
|
self.on_ping = on_ping
|
||||||
|
self.on_pong = on_pong
|
||||||
|
self.on_cont_message = on_cont_message
|
||||||
|
self.keep_running = keep_running
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
self.sock = None
|
||||||
|
self.last_ping_tm = 0
|
||||||
|
self.subprotocols = subprotocols
|
||||||
|
|
||||||
|
def send(self, data, opcode=ABNF.OPCODE_TEXT):
|
||||||
|
"""
|
||||||
|
send message.
|
||||||
|
data: message to send. If you set opcode to OPCODE_TEXT,
|
||||||
|
data must be utf-8 string or unicode.
|
||||||
|
opcode: operation code of data. default is OPCODE_TEXT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.sock or self.sock.send(data, opcode) == 0:
|
||||||
|
raise WebSocketConnectionClosedException("Connection is already closed.")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
close websocket connection.
|
||||||
|
"""
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
def _send_ping(self, interval, event):
|
||||||
|
while not event.wait(interval):
|
||||||
|
self.last_ping_tm = time.time()
|
||||||
|
if self.sock:
|
||||||
|
self.sock.ping()
|
||||||
|
|
||||||
|
def run_forever(self, sockopt=None, sslopt=None,
|
||||||
|
ping_interval=0, ping_timeout=None,
|
||||||
|
http_proxy_host=None, http_proxy_port=None,
|
||||||
|
http_no_proxy=None, http_proxy_auth=None,
|
||||||
|
skip_utf8_validation=False,
|
||||||
|
host=None, origin=None):
|
||||||
|
"""
|
||||||
|
run event loop for WebSocket framework.
|
||||||
|
This loop is infinite loop and is alive during websocket is available.
|
||||||
|
sockopt: values for socket.setsockopt.
|
||||||
|
sockopt must be tuple
|
||||||
|
and each element is argument of sock.setscokopt.
|
||||||
|
sslopt: ssl socket optional dict.
|
||||||
|
ping_interval: automatically send "ping" command
|
||||||
|
every specified period(second)
|
||||||
|
if set to 0, not send automatically.
|
||||||
|
ping_timeout: timeout(second) if the pong message is not recieved.
|
||||||
|
http_proxy_host: http proxy host name.
|
||||||
|
http_proxy_port: http proxy port. If not set, set to 80.
|
||||||
|
http_no_proxy: host names, which doesn't use proxy.
|
||||||
|
skip_utf8_validation: skip utf8 validation.
|
||||||
|
host: update host header.
|
||||||
|
origin: update origin header.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ping_timeout or ping_timeout <= 0:
|
||||||
|
ping_timeout = None
|
||||||
|
if sockopt is None:
|
||||||
|
sockopt = []
|
||||||
|
if sslopt is None:
|
||||||
|
sslopt = {}
|
||||||
|
if self.sock:
|
||||||
|
raise WebSocketException("socket is already opened")
|
||||||
|
thread = None
|
||||||
|
close_frame = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sock = WebSocket(self.get_mask_key,
|
||||||
|
sockopt=sockopt, sslopt=sslopt,
|
||||||
|
fire_cont_frame=self.on_cont_message and True or False,
|
||||||
|
skip_utf8_validation=skip_utf8_validation)
|
||||||
|
self.sock.settimeout(getdefaulttimeout())
|
||||||
|
self.sock.connect(self.url, header=self.header, cookie=self.cookie,
|
||||||
|
http_proxy_host=http_proxy_host,
|
||||||
|
http_proxy_port=http_proxy_port,
|
||||||
|
http_no_proxy=http_no_proxy, http_proxy_auth=http_proxy_auth,
|
||||||
|
subprotocols=self.subprotocols,
|
||||||
|
host=host, origin=origin)
|
||||||
|
self._callback(self.on_open)
|
||||||
|
|
||||||
|
if ping_interval:
|
||||||
|
event = threading.Event()
|
||||||
|
thread = threading.Thread(target=self._send_ping, args=(ping_interval, event))
|
||||||
|
thread.setDaemon(True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
while self.sock.connected:
|
||||||
|
r, w, e = select.select((self.sock.sock, ), (), (), ping_timeout)
|
||||||
|
if not self.keep_running:
|
||||||
|
break
|
||||||
|
if ping_timeout and self.last_ping_tm and time.time() - self.last_ping_tm > ping_timeout:
|
||||||
|
self.last_ping_tm = 0
|
||||||
|
raise WebSocketTimeoutException("ping timed out")
|
||||||
|
|
||||||
|
if r:
|
||||||
|
op_code, frame = self.sock.recv_data_frame(True)
|
||||||
|
if op_code == ABNF.OPCODE_CLOSE:
|
||||||
|
close_frame = frame
|
||||||
|
break
|
||||||
|
elif op_code == ABNF.OPCODE_PING:
|
||||||
|
self._callback(self.on_ping, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_PONG:
|
||||||
|
self._callback(self.on_pong, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
|
||||||
|
self._callback(self.on_cont_message, frame.data, frame.fin)
|
||||||
|
else:
|
||||||
|
data = frame.data
|
||||||
|
if six.PY3 and frame.opcode == ABNF.OPCODE_TEXT:
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
self._callback(self.on_message, data)
|
||||||
|
except Exception as e:
|
||||||
|
self._callback(self.on_error, e)
|
||||||
|
finally:
|
||||||
|
if thread:
|
||||||
|
event.set()
|
||||||
|
thread.join()
|
||||||
|
self.keep_running = False
|
||||||
|
self.sock.close()
|
||||||
|
self._callback(self.on_close,
|
||||||
|
*self._get_close_args(close_frame.data if close_frame else None))
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def _get_close_args(self, data):
|
||||||
|
""" this functions extracts the code, reason from the close body
|
||||||
|
if they exists, and if the self.on_close except three arguments """
|
||||||
|
import inspect
|
||||||
|
# if the on_close callback is "old", just return empty list
|
||||||
|
if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if data and len(data) >= 2:
|
||||||
|
code = 256*six.byte2int(data[0:1]) + six.byte2int(data[1:2])
|
||||||
|
reason = data[2:].decode('utf-8')
|
||||||
|
return [code, reason]
|
||||||
|
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
|
def _callback(self, callback, *args):
|
||||||
|
if callback:
|
||||||
|
try:
|
||||||
|
callback(self, *args)
|
||||||
|
except Exception as e:
|
||||||
|
error(e)
|
||||||
|
if isEnabledForDebug():
|
||||||
|
_, _, tb = sys.exc_info()
|
||||||
|
traceback.print_tb(tb)
|
||||||
482
lib/websocket/_core.py
Normal file
482
lib/websocket/_core.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# websocket modules
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._abnf import *
|
||||||
|
from ._socket import *
|
||||||
|
from ._utils import *
|
||||||
|
from ._url import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._http import *
|
||||||
|
from ._handshake import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
|
||||||
|
"""
|
||||||
|
websocket python client.
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This version support only hybi-13.
|
||||||
|
Please see http://tools.ietf.org/html/rfc6455 for protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def create_connection(url, timeout=None, **options):
|
||||||
|
"""
|
||||||
|
connect to url and return websocket object.
|
||||||
|
|
||||||
|
Connect to url and return the WebSocket object.
|
||||||
|
Passing optional timeout parameter will set the timeout on the socket.
|
||||||
|
If no timeout is supplied,
|
||||||
|
the global default timeout setting returned by getdefauttimeout() is used.
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> conn = create_connection("ws://echo.websocket.org/",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
|
||||||
|
timeout: socket timeout time. This value is integer.
|
||||||
|
if you set None for this value,
|
||||||
|
it means "use default_timeout value"
|
||||||
|
|
||||||
|
|
||||||
|
options: "header" -> custom http header list.
|
||||||
|
"cookie" -> cookie value.
|
||||||
|
"origin" -> custom origin url.
|
||||||
|
"host" -> custom host header string.
|
||||||
|
"http_proxy_host" - http proxy host name.
|
||||||
|
"http_proxy_port" - http proxy port. If not set, set to 80.
|
||||||
|
"http_no_proxy" - host names, which doesn't use proxy.
|
||||||
|
"http_proxy_auth" - http proxy auth infomation.
|
||||||
|
tuple of username and password.
|
||||||
|
default is None
|
||||||
|
"enable_multithread" -> enable lock for multithread.
|
||||||
|
"sockopt" -> socket options
|
||||||
|
"sslopt" -> ssl option
|
||||||
|
"subprotocols" - array of available sub protocols.
|
||||||
|
default is None.
|
||||||
|
"skip_utf8_validation" - skip utf8 validation.
|
||||||
|
"""
|
||||||
|
sockopt = options.get("sockopt", [])
|
||||||
|
sslopt = options.get("sslopt", {})
|
||||||
|
fire_cont_frame = options.get("fire_cont_frame", False)
|
||||||
|
enable_multithread = options.get("enable_multithread", False)
|
||||||
|
skip_utf8_validation = options.get("skip_utf8_validation", False)
|
||||||
|
websock = WebSocket(sockopt=sockopt, sslopt=sslopt,
|
||||||
|
fire_cont_frame=fire_cont_frame,
|
||||||
|
enable_multithread=enable_multithread,
|
||||||
|
skip_utf8_validation=skip_utf8_validation)
|
||||||
|
websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
|
||||||
|
websock.connect(url, **options)
|
||||||
|
return websock
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocket(object):
|
||||||
|
"""
|
||||||
|
Low level WebSocket interface.
|
||||||
|
This class is based on
|
||||||
|
The WebSocket protocol draft-hixie-thewebsocketprotocol-76
|
||||||
|
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||||
|
|
||||||
|
We can connect to the websocket server and send/recieve data.
|
||||||
|
The following example is a echo client.
|
||||||
|
|
||||||
|
>>> import websocket
|
||||||
|
>>> ws = websocket.WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.org")
|
||||||
|
>>> ws.send("Hello, Server")
|
||||||
|
>>> ws.recv()
|
||||||
|
'Hello, Server'
|
||||||
|
>>> ws.close()
|
||||||
|
|
||||||
|
get_mask_key: a callable to produce new mask keys, see the set_mask_key
|
||||||
|
function's docstring for more details
|
||||||
|
sockopt: values for socket.setsockopt.
|
||||||
|
sockopt must be tuple and each element is argument of sock.setscokopt.
|
||||||
|
sslopt: dict object for ssl socket option.
|
||||||
|
fire_cont_frame: fire recv event for each cont frame. default is False
|
||||||
|
enable_multithread: if set to True, lock send method.
|
||||||
|
skip_utf8_validation: skip utf8 validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None,
|
||||||
|
fire_cont_frame=False, enable_multithread=False,
|
||||||
|
skip_utf8_validation=False):
|
||||||
|
"""
|
||||||
|
Initalize WebSocket object.
|
||||||
|
"""
|
||||||
|
self.sock_opt = sock_opt(sockopt, sslopt)
|
||||||
|
self.handshake_response = None
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
self.connected = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
# These buffer over the build-up of a single frame.
|
||||||
|
self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
|
||||||
|
self.cont_frame = continuous_frame(fire_cont_frame, skip_utf8_validation)
|
||||||
|
|
||||||
|
if enable_multithread:
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
else:
|
||||||
|
self.lock = NoLock()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Allow iteration over websocket, implying sequential `recv` executions.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
yield self.recv()
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self.recv()
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
return self.__next__()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
return self.sock.fileno()
|
||||||
|
|
||||||
|
def set_mask_key(self, func):
|
||||||
|
"""
|
||||||
|
set function to create musk key. You can custumize mask key generator.
|
||||||
|
Mainly, this is for testing purpose.
|
||||||
|
|
||||||
|
func: callable object. the fuct must 1 argument as integer.
|
||||||
|
The argument means length of mask key.
|
||||||
|
This func must be return string(byte array),
|
||||||
|
which length is argument specified.
|
||||||
|
"""
|
||||||
|
self.get_mask_key = func
|
||||||
|
|
||||||
|
def gettimeout(self):
|
||||||
|
"""
|
||||||
|
Get the websocket timeout(second).
|
||||||
|
"""
|
||||||
|
return self.sock_opt.timeout
|
||||||
|
|
||||||
|
def settimeout(self, timeout):
|
||||||
|
"""
|
||||||
|
Set the timeout to the websocket.
|
||||||
|
|
||||||
|
timeout: timeout time(second).
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = timeout
|
||||||
|
if self.sock:
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
|
||||||
|
timeout = property(gettimeout, settimeout)
|
||||||
|
|
||||||
|
def getsubprotocol(self):
|
||||||
|
"""
|
||||||
|
get subprotocol
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.subprotocol
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprotocol = property(getsubprotocol)
|
||||||
|
|
||||||
|
def getstatus(self):
|
||||||
|
"""
|
||||||
|
get handshake status
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.status
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
status = property(getstatus)
|
||||||
|
|
||||||
|
def getheaders(self):
|
||||||
|
"""
|
||||||
|
get handshake response header
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.headers
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
headers = property(getheaders)
|
||||||
|
|
||||||
|
def connect(self, url, **options):
|
||||||
|
"""
|
||||||
|
Connect to url. url is websocket url scheme.
|
||||||
|
ie. ws://host:port/resource
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> ws = WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.org/",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
timeout: socket timeout time. This value is integer.
|
||||||
|
if you set None for this value,
|
||||||
|
it means "use default_timeout value"
|
||||||
|
|
||||||
|
options: "header" -> custom http header list.
|
||||||
|
"cookie" -> cookie value.
|
||||||
|
"origin" -> custom origin url.
|
||||||
|
"host" -> custom host header string.
|
||||||
|
"http_proxy_host" - http proxy host name.
|
||||||
|
"http_proxy_port" - http proxy port. If not set, set to 80.
|
||||||
|
"http_no_proxy" - host names, which doesn't use proxy.
|
||||||
|
"http_proxy_auth" - http proxy auth infomation.
|
||||||
|
tuple of username and password.
|
||||||
|
defualt is None
|
||||||
|
"subprotocols" - array of available sub protocols.
|
||||||
|
default is None.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.handshake_response = handshake(self.sock, *addrs, **options)
|
||||||
|
self.connected = True
|
||||||
|
except:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
|
||||||
|
"""
|
||||||
|
Send the data as string.
|
||||||
|
|
||||||
|
payload: Payload must be utf-8 string or unicode,
|
||||||
|
if the opcode is OPCODE_TEXT.
|
||||||
|
Otherwise, it must be string(byte array)
|
||||||
|
|
||||||
|
opcode: operation code to send. Please see OPCODE_XXX.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame = ABNF.create_frame(payload, opcode)
|
||||||
|
return self.send_frame(frame)
|
||||||
|
|
||||||
|
def send_frame(self, frame):
|
||||||
|
"""
|
||||||
|
Send the data frame.
|
||||||
|
|
||||||
|
frame: frame data created by ABNF.create_frame
|
||||||
|
|
||||||
|
>>> ws = create_connection("ws://echo.websocket.org/")
|
||||||
|
>>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.get_mask_key:
|
||||||
|
frame.get_mask_key = self.get_mask_key
|
||||||
|
data = frame.format()
|
||||||
|
length = len(data)
|
||||||
|
trace("send: " + repr(data))
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
while data:
|
||||||
|
l = self._send(data)
|
||||||
|
data = data[l:]
|
||||||
|
|
||||||
|
return length
|
||||||
|
|
||||||
|
def send_binary(self, payload):
|
||||||
|
return self.send(payload, ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
|
def ping(self, payload=""):
|
||||||
|
"""
|
||||||
|
send ping data.
|
||||||
|
|
||||||
|
payload: data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, six.text_type):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PING)
|
||||||
|
|
||||||
|
def pong(self, payload):
|
||||||
|
"""
|
||||||
|
send pong data.
|
||||||
|
|
||||||
|
payload: data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, six.text_type):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PONG)
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
"""
|
||||||
|
Receive string data(byte array) from the server.
|
||||||
|
|
||||||
|
return value: string(byte array) value.
|
||||||
|
"""
|
||||||
|
opcode, data = self.recv_data()
|
||||||
|
if six.PY3 and opcode == ABNF.OPCODE_TEXT:
|
||||||
|
return data.decode("utf-8")
|
||||||
|
elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY:
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def recv_data(self, control_frame=False):
|
||||||
|
"""
|
||||||
|
Recieve data with operation code.
|
||||||
|
|
||||||
|
control_frame: a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
return value: tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
opcode, frame = self.recv_data_frame(control_frame)
|
||||||
|
return opcode, frame.data
|
||||||
|
|
||||||
|
def recv_data_frame(self, control_frame=False):
|
||||||
|
"""
|
||||||
|
Recieve data with operation code.
|
||||||
|
|
||||||
|
control_frame: a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
return value: tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if not frame:
|
||||||
|
# handle error:
|
||||||
|
# 'NoneType' object has no attribute 'opcode'
|
||||||
|
raise WebSocketProtocolException("Not a valid frame %s" % frame)
|
||||||
|
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
|
||||||
|
self.cont_frame.validate(frame)
|
||||||
|
self.cont_frame.add(frame)
|
||||||
|
|
||||||
|
if self.cont_frame.is_fire(frame):
|
||||||
|
return self.cont_frame.extract(frame)
|
||||||
|
|
||||||
|
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
self.send_close()
|
||||||
|
return (frame.opcode, frame)
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PING:
|
||||||
|
if len(frame.data) < 126:
|
||||||
|
self.pong(frame.data)
|
||||||
|
else:
|
||||||
|
raise WebSocketProtocolException("Ping message is too long")
|
||||||
|
if control_frame:
|
||||||
|
return (frame.opcode, frame)
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PONG:
|
||||||
|
if control_frame:
|
||||||
|
return (frame.opcode, frame)
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
"""
|
||||||
|
recieve data as frame from server.
|
||||||
|
|
||||||
|
return value: ABNF frame object.
|
||||||
|
"""
|
||||||
|
return self.frame_buffer.recv_frame()
|
||||||
|
|
||||||
|
def send_close(self, status=STATUS_NORMAL, reason=six.b("")):
|
||||||
|
"""
|
||||||
|
send close data to the server.
|
||||||
|
|
||||||
|
status: status code to send. see STATUS_XXX.
|
||||||
|
|
||||||
|
reason: the reason to close. This must be string or bytes.
|
||||||
|
"""
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
|
||||||
|
def close(self, status=STATUS_NORMAL, reason=six.b("")):
|
||||||
|
"""
|
||||||
|
Close Websocket object
|
||||||
|
|
||||||
|
status: status code to send. see STATUS_XXX.
|
||||||
|
|
||||||
|
reason: the reason to close. This must be string.
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
timeout = self.sock.gettimeout()
|
||||||
|
self.sock.settimeout(3)
|
||||||
|
try:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if isEnabledForError():
|
||||||
|
recv_status = struct.unpack("!H", frame.data)[0]
|
||||||
|
if recv_status != STATUS_NORMAL:
|
||||||
|
error("close status: " + repr(recv_status))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"""
|
||||||
|
Low-level asynchonous abort, wakes up other threads that are waiting in recv_*
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"close socket, immediately."
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _send(self, data):
|
||||||
|
return send(self.sock, data)
|
||||||
|
|
||||||
|
def _recv(self, bufsize):
|
||||||
|
try:
|
||||||
|
return recv(self.sock, bufsize)
|
||||||
|
except WebSocketConnectionClosedException:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
raise
|
||||||
65
lib/websocket/_exceptions.py
Normal file
65
lib/websocket/_exceptions.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
define websocket exceptions
|
||||||
|
"""
|
||||||
|
|
||||||
|
class WebSocketException(Exception):
|
||||||
|
"""
|
||||||
|
websocket exeception class.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WebSocketProtocolException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the webscoket protocol is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WebSocketPayloadException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the webscoket payload is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WebSocketConnectionClosedException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If remote host closed the connection or some network error happened,
|
||||||
|
this exception will be raised.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WebSocketTimeoutException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketTimeoutException will be raised at socket timeout during read/write data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WebSocketProxyException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketProxyException will be raised when proxy error occured.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
155
lib/websocket/_handshake.py
Normal file
155
lib/websocket/_handshake.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import six
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from ._logging import *
|
||||||
|
from ._url import *
|
||||||
|
from ._socket import*
|
||||||
|
from ._http import *
|
||||||
|
from ._exceptions import *
|
||||||
|
|
||||||
|
__all__ = ["handshake_response", "handshake"]
|
||||||
|
|
||||||
|
# websocket supported version.
|
||||||
|
VERSION = 13
|
||||||
|
|
||||||
|
|
||||||
|
class handshake_response(object):
|
||||||
|
def __init__(self, status, headers, subprotocol):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
self.subprotocol = subprotocol
|
||||||
|
|
||||||
|
|
||||||
|
def handshake(sock, hostname, port, resource, **options):
|
||||||
|
headers, key = _get_handshake_headers(resource, hostname, port, options)
|
||||||
|
|
||||||
|
header_str = "\r\n".join(headers)
|
||||||
|
send(sock, header_str)
|
||||||
|
dump("request header", header_str)
|
||||||
|
|
||||||
|
status, resp = _get_resp_headers(sock)
|
||||||
|
success, subproto = _validate(resp, key, options.get("subprotocols"))
|
||||||
|
if not success:
|
||||||
|
raise WebSocketException("Invalid WebSocket Header")
|
||||||
|
|
||||||
|
return handshake_response(status, resp, subproto)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_handshake_headers(resource, host, port, options):
|
||||||
|
headers = []
|
||||||
|
headers.append("GET %s HTTP/1.1" % resource)
|
||||||
|
headers.append("Upgrade: websocket")
|
||||||
|
headers.append("Connection: Upgrade")
|
||||||
|
if port == 80:
|
||||||
|
hostport = host
|
||||||
|
else:
|
||||||
|
hostport = "%s:%d" % (host, port)
|
||||||
|
|
||||||
|
if "host" in options and options["host"]:
|
||||||
|
headers.append("Host: %s" % options["host"])
|
||||||
|
else:
|
||||||
|
headers.append("Host: %s" % hostport)
|
||||||
|
|
||||||
|
if "origin" in options and options["origin"]:
|
||||||
|
headers.append("Origin: %s" % options["origin"])
|
||||||
|
else:
|
||||||
|
headers.append("Origin: http://%s" % hostport)
|
||||||
|
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
headers.append("Sec-WebSocket-Key: %s" % key)
|
||||||
|
headers.append("Sec-WebSocket-Version: %s" % VERSION)
|
||||||
|
|
||||||
|
subprotocols = options.get("subprotocols")
|
||||||
|
if subprotocols:
|
||||||
|
headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols))
|
||||||
|
|
||||||
|
if "header" in options:
|
||||||
|
headers.extend(options["header"])
|
||||||
|
|
||||||
|
cookie = options.get("cookie", None)
|
||||||
|
|
||||||
|
if cookie:
|
||||||
|
headers.append("Cookie: %s" % cookie)
|
||||||
|
|
||||||
|
headers.append("")
|
||||||
|
headers.append("")
|
||||||
|
|
||||||
|
return headers, key
|
||||||
|
|
||||||
|
|
||||||
|
def _get_resp_headers(sock, success_status=101):
|
||||||
|
status, resp_headers = read_headers(sock)
|
||||||
|
if status != success_status:
|
||||||
|
raise WebSocketException("Handshake status %d" % status)
|
||||||
|
return status, resp_headers
|
||||||
|
|
||||||
|
_HEADERS_TO_CHECK = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate(headers, key, subprotocols):
|
||||||
|
subproto = None
|
||||||
|
for k, v in _HEADERS_TO_CHECK.items():
|
||||||
|
r = headers.get(k, None)
|
||||||
|
if not r:
|
||||||
|
return False, None
|
||||||
|
r = r.lower()
|
||||||
|
if v != r:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
if subprotocols:
|
||||||
|
subproto = headers.get("sec-websocket-protocol", None).lower()
|
||||||
|
if not subproto or subproto not in [s.lower() for s in subprotocols]:
|
||||||
|
error("Invalid subprotocol: " + str(subprotocols))
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
result = headers.get("sec-websocket-accept", None)
|
||||||
|
if not result:
|
||||||
|
return False, None
|
||||||
|
result = result.lower()
|
||||||
|
|
||||||
|
if isinstance(result, six.text_type):
|
||||||
|
result = result.encode('utf-8')
|
||||||
|
|
||||||
|
value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
|
||||||
|
hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
|
||||||
|
success = (hashed == result)
|
||||||
|
if success:
|
||||||
|
return True, subproto
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_sec_websocket_key():
|
||||||
|
uid = uuid.uuid4()
|
||||||
|
return base64encode(uid.bytes).decode('utf-8').strip()
|
||||||
215
lib/websocket/_http.py
Normal file
215
lib/websocket/_http.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
else:
|
||||||
|
from base64 import encodestring as base64encode
|
||||||
|
|
||||||
|
from ._logging import *
|
||||||
|
from ._url import *
|
||||||
|
from ._socket import*
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
|
||||||
|
__all__ = ["proxy_info", "connect", "read_headers"]
|
||||||
|
|
||||||
|
class proxy_info(object):
|
||||||
|
def __init__(self, **options):
|
||||||
|
self.host = options.get("http_proxy_host", None)
|
||||||
|
if self.host:
|
||||||
|
self.port = options.get("http_proxy_port", 0)
|
||||||
|
self.auth = options.get("http_proxy_auth", None)
|
||||||
|
self.no_proxy = options.get("http_no_proxy", None)
|
||||||
|
else:
|
||||||
|
self.port = 0
|
||||||
|
self.auth = None
|
||||||
|
self.no_proxy = None
|
||||||
|
|
||||||
|
def connect(url, options, proxy):
|
||||||
|
hostname, port, resource, is_secure = parse_url(url)
|
||||||
|
addrinfo_list, need_tunnel, auth = _get_addrinfo_list(hostname, port, is_secure, proxy)
|
||||||
|
if not addrinfo_list:
|
||||||
|
raise WebSocketException(
|
||||||
|
"Host not found.: " + hostname + ":" + str(port))
|
||||||
|
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
|
||||||
|
if need_tunnel:
|
||||||
|
sock = _tunnel(sock, hostname, port, auth)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port, resource)
|
||||||
|
except:
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _get_addrinfo_list(hostname, port, is_secure, proxy):
|
||||||
|
phost, pport, pauth = get_proxy_info(hostname, is_secure,
|
||||||
|
proxy.host, proxy.port, proxy.auth, proxy.no_proxy)
|
||||||
|
if not phost:
|
||||||
|
addrinfo_list = socket.getaddrinfo(hostname, port, 0, 0, socket.SOL_TCP)
|
||||||
|
return addrinfo_list, False, None
|
||||||
|
else:
|
||||||
|
pport = pport and pport or 80
|
||||||
|
addrinfo_list = socket.getaddrinfo(phost, pport, 0, 0, socket.SOL_TCP)
|
||||||
|
return addrinfo_list, True, pauth
|
||||||
|
|
||||||
|
|
||||||
|
def _open_socket(addrinfo_list, sockopt, timeout):
|
||||||
|
err = None
|
||||||
|
for addrinfo in addrinfo_list:
|
||||||
|
family = addrinfo[0]
|
||||||
|
sock = socket.socket(family)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
for opts in DEFAULT_SOCKET_OPTION:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
for opts in sockopt:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
|
||||||
|
address = addrinfo[4]
|
||||||
|
try:
|
||||||
|
sock.connect(address)
|
||||||
|
except socket.error as error:
|
||||||
|
error.remote_ip = str(address[0])
|
||||||
|
if error.errno in (errno.ECONNREFUSED, ):
|
||||||
|
err = error
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _can_use_sni():
|
||||||
|
return (six.PY2 and sys.version_info[1] >= 7 and sys.version_info[2] >= 9) or (six.PY3 and sys.version_info[2] >= 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
|
||||||
|
context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
|
||||||
|
|
||||||
|
context.load_verify_locations(cafile=sslopt.get('ca_certs', None))
|
||||||
|
# see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
|
||||||
|
context.verify_mode = sslopt['cert_reqs']
|
||||||
|
if HAVE_CONTEXT_CHECK_HOSTNAME:
|
||||||
|
context.check_hostname = check_hostname
|
||||||
|
if 'ciphers' in sslopt:
|
||||||
|
context.set_ciphers(sslopt['ciphers'])
|
||||||
|
|
||||||
|
return context.wrap_socket(
|
||||||
|
sock,
|
||||||
|
do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
|
||||||
|
suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
|
||||||
|
server_hostname=hostname,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ssl_socket(sock, user_sslopt, hostname):
|
||||||
|
sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
|
||||||
|
certPath = os.path.join(
|
||||||
|
os.path.dirname(__file__), "cacert.pem")
|
||||||
|
if os.path.isfile(certPath):
|
||||||
|
sslopt['ca_certs'] = certPath
|
||||||
|
sslopt.update(user_sslopt)
|
||||||
|
check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop('check_hostname', True)
|
||||||
|
|
||||||
|
if _can_use_sni():
|
||||||
|
sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
|
||||||
|
else:
|
||||||
|
sslopt.pop('check_hostname', True)
|
||||||
|
sock = ssl.wrap_socket(sock, **sslopt)
|
||||||
|
|
||||||
|
if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname:
|
||||||
|
match_hostname(sock.getpeercert(), hostname)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
def _tunnel(sock, host, port, auth):
|
||||||
|
debug("Connecting proxy...")
|
||||||
|
connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port)
|
||||||
|
# TODO: support digest auth.
|
||||||
|
if auth and auth[0]:
|
||||||
|
auth_str = auth[0]
|
||||||
|
if auth[1]:
|
||||||
|
auth_str += ":" + auth[1]
|
||||||
|
encoded_str = base64encode(auth_str.encode()).strip().decode()
|
||||||
|
connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str
|
||||||
|
connect_header += "\r\n"
|
||||||
|
dump("request header", connect_header)
|
||||||
|
|
||||||
|
send(sock, connect_header)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status, resp_headers = read_headers(sock)
|
||||||
|
except Exception as e:
|
||||||
|
raise WebSocketProxyException(str(e))
|
||||||
|
|
||||||
|
if status != 200:
|
||||||
|
raise WebSocketProxyException(
|
||||||
|
"failed CONNECT via proxy status: %r" + status)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
def read_headers(sock):
|
||||||
|
status = None
|
||||||
|
headers = {}
|
||||||
|
trace("--- response header ---")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = recv_line(sock)
|
||||||
|
line = line.decode('utf-8').strip()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
trace(line)
|
||||||
|
if not status:
|
||||||
|
|
||||||
|
status_info = line.split(" ", 2)
|
||||||
|
status = int(status_info[1])
|
||||||
|
else:
|
||||||
|
kv = line.split(":", 1)
|
||||||
|
if len(kv) == 2:
|
||||||
|
key, value = kv
|
||||||
|
headers[key.lower()] = value.strip().lower()
|
||||||
|
else:
|
||||||
|
raise WebSocketException("Invalid header")
|
||||||
|
|
||||||
|
trace("-----------------------")
|
||||||
|
|
||||||
|
return status, headers
|
||||||
71
lib/websocket/_logging.py
Normal file
71
lib/websocket/_logging.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger()
|
||||||
|
_traceEnabled = False
|
||||||
|
|
||||||
|
__all__ = ["enableTrace", "dump", "error", "debug", "trace",
|
||||||
|
"isEnabledForError", "isEnabledForDebug"]
|
||||||
|
|
||||||
|
|
||||||
|
def enableTrace(tracable):
|
||||||
|
"""
|
||||||
|
turn on/off the tracability.
|
||||||
|
|
||||||
|
tracable: boolean value. if set True, tracability is enabled.
|
||||||
|
"""
|
||||||
|
global _traceEnabled
|
||||||
|
_traceEnabled = tracable
|
||||||
|
if tracable:
|
||||||
|
if not _logger.handlers:
|
||||||
|
_logger.addHandler(logging.StreamHandler())
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def dump(title, message):
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug("--- " + title + " ---")
|
||||||
|
_logger.debug(message)
|
||||||
|
_logger.debug("-----------------------")
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg):
|
||||||
|
_logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg):
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def trace(msg):
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForError():
|
||||||
|
return _logger.isEnabledFor(logging.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForDebug():
|
||||||
|
return _logger.isEnabledFor(logging.DEBUG)
|
||||||
121
lib/websocket/_socket.py
Normal file
121
lib/websocket/_socket.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import six
|
||||||
|
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._utils import *
|
||||||
|
from ._ssl_compat import *
|
||||||
|
|
||||||
|
DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
|
||||||
|
if hasattr(socket, "SO_KEEPALIVE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
|
||||||
|
if hasattr(socket, "TCP_KEEPIDLE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
|
||||||
|
if hasattr(socket, "TCP_KEEPINTVL"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
|
||||||
|
if hasattr(socket, "TCP_KEEPCNT"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
|
||||||
|
|
||||||
|
_default_timeout = None
|
||||||
|
|
||||||
|
__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout",
|
||||||
|
"recv", "recv_line", "send"]
|
||||||
|
|
||||||
|
class sock_opt(object):
|
||||||
|
def __init__(self, sockopt, sslopt):
|
||||||
|
if sockopt is None:
|
||||||
|
sockopt = []
|
||||||
|
if sslopt is None:
|
||||||
|
sslopt = {}
|
||||||
|
self.sockopt = sockopt
|
||||||
|
self.sslopt = sslopt
|
||||||
|
self.timeout = None
|
||||||
|
|
||||||
|
def setdefaulttimeout(timeout):
|
||||||
|
"""
|
||||||
|
Set the global timeout setting to connect.
|
||||||
|
|
||||||
|
timeout: default socket timeout time. This value is second.
|
||||||
|
"""
|
||||||
|
global _default_timeout
|
||||||
|
_default_timeout = timeout
|
||||||
|
|
||||||
|
|
||||||
|
def getdefaulttimeout():
|
||||||
|
"""
|
||||||
|
Return the global timeout setting(second) to connect.
|
||||||
|
"""
|
||||||
|
return _default_timeout
|
||||||
|
|
||||||
|
|
||||||
|
def recv(sock, bufsize):
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
bytes = sock.recv(bufsize)
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except SSLError as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if message == "The read operation timed out":
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not bytes:
|
||||||
|
raise WebSocketConnectionClosedException("Connection is already closed.")
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
|
||||||
|
|
||||||
|
def recv_line(sock):
|
||||||
|
line = []
|
||||||
|
while True:
|
||||||
|
c = recv(sock, 1)
|
||||||
|
line.append(c)
|
||||||
|
if c == six.b("\n"):
|
||||||
|
break
|
||||||
|
return six.b("").join(line)
|
||||||
|
|
||||||
|
|
||||||
|
def send(sock, data):
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return sock.send(data)
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except Exception as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if message and "timed out" in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
45
lib/websocket/_ssl_compat.py
Normal file
45
lib/websocket/_ssl_compat.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ["HAVE_SSL", "ssl", "SSLError"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
from ssl import SSLError
|
||||||
|
if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'):
|
||||||
|
HAVE_CONTEXT_CHECK_HOSTNAME = True
|
||||||
|
else:
|
||||||
|
HAVE_CONTEXT_CHECK_HOSTNAME = False
|
||||||
|
if hasattr(ssl, "match_hostname"):
|
||||||
|
from ssl import match_hostname
|
||||||
|
else:
|
||||||
|
from backports.ssl_match_hostname import match_hostname
|
||||||
|
__all__.append("match_hostname")
|
||||||
|
__all__.append("HAVE_CONTEXT_CHECK_HOSTNAME")
|
||||||
|
|
||||||
|
HAVE_SSL = True
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for ssl none-support environment.
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
HAVE_SSL = False
|
||||||
126
lib/websocket/_url.py
Normal file
126
lib/websocket/_url.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from six.moves.urllib.parse import urlparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
__all__ = ["parse_url", "get_proxy_info"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
parse url and the result is tuple of
|
||||||
|
(hostname, port, resource path and the flag of secure mode)
|
||||||
|
|
||||||
|
url: url string.
|
||||||
|
"""
|
||||||
|
if ":" not in url:
|
||||||
|
raise ValueError("url is invalid")
|
||||||
|
|
||||||
|
scheme, url = url.split(":", 1)
|
||||||
|
|
||||||
|
parsed = urlparse(url, scheme="ws")
|
||||||
|
if parsed.hostname:
|
||||||
|
hostname = parsed.hostname
|
||||||
|
else:
|
||||||
|
raise ValueError("hostname is invalid")
|
||||||
|
port = 0
|
||||||
|
if parsed.port:
|
||||||
|
port = parsed.port
|
||||||
|
|
||||||
|
is_secure = False
|
||||||
|
if scheme == "ws":
|
||||||
|
if not port:
|
||||||
|
port = 80
|
||||||
|
elif scheme == "wss":
|
||||||
|
is_secure = True
|
||||||
|
if not port:
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
raise ValueError("scheme %s is invalid" % scheme)
|
||||||
|
|
||||||
|
if parsed.path:
|
||||||
|
resource = parsed.path
|
||||||
|
else:
|
||||||
|
resource = "/"
|
||||||
|
|
||||||
|
if parsed.query:
|
||||||
|
resource += "?" + parsed.query
|
||||||
|
|
||||||
|
return (hostname, port, resource, is_secure)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
if not no_proxy:
|
||||||
|
v = os.environ.get("no_proxy", "").replace(" ", "")
|
||||||
|
no_proxy = v.split(",")
|
||||||
|
if not no_proxy:
|
||||||
|
no_proxy = DEFAULT_NO_PROXY_HOST
|
||||||
|
|
||||||
|
return hostname in no_proxy
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_info(hostname, is_secure,
|
||||||
|
proxy_host=None, proxy_port=0, proxy_auth=None, no_proxy=None):
|
||||||
|
"""
|
||||||
|
try to retrieve proxy host and port from environment
|
||||||
|
if not provided in options.
|
||||||
|
result is (proxy_host, proxy_port, proxy_auth).
|
||||||
|
proxy_auth is tuple of username and password
|
||||||
|
of proxy authentication information.
|
||||||
|
|
||||||
|
hostname: websocket server name.
|
||||||
|
|
||||||
|
is_secure: is the connection secure? (wss)
|
||||||
|
looks for "https_proxy" in env
|
||||||
|
before falling back to "http_proxy"
|
||||||
|
|
||||||
|
options: "http_proxy_host" - http proxy host name.
|
||||||
|
"http_proxy_port" - http proxy port.
|
||||||
|
"http_no_proxy" - host names, which doesn't use proxy.
|
||||||
|
"http_proxy_auth" - http proxy auth infomation.
|
||||||
|
tuple of username and password.
|
||||||
|
defualt is None
|
||||||
|
"""
|
||||||
|
if _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
return None, 0, None
|
||||||
|
|
||||||
|
if proxy_host:
|
||||||
|
port = proxy_port
|
||||||
|
auth = proxy_auth
|
||||||
|
return proxy_host, port, auth
|
||||||
|
|
||||||
|
env_keys = ["http_proxy"]
|
||||||
|
if is_secure:
|
||||||
|
env_keys.insert(0, "https_proxy")
|
||||||
|
|
||||||
|
for key in env_keys:
|
||||||
|
value = os.environ.get(key, None)
|
||||||
|
if value:
|
||||||
|
proxy = urlparse(value)
|
||||||
|
auth = (proxy.username, proxy.password) if proxy.username else None
|
||||||
|
return proxy.hostname, proxy.port, auth
|
||||||
|
|
||||||
|
return None, 0, None
|
||||||
88
lib/websocket/_utils.py
Normal file
88
lib/websocket/_utils.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||||
|
Boston, MA 02110-1335 USA
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = ["NoLock", "validate_utf8", "extract_err_message"]
|
||||||
|
|
||||||
|
class NoLock(object):
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self,type, value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# UTF-8 validator
|
||||||
|
# python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||||
|
|
||||||
|
UTF8_ACCEPT = 0
|
||||||
|
UTF8_REJECT=12
|
||||||
|
|
||||||
|
_UTF8D = [
|
||||||
|
# The first part of the table maps bytes to character classes that
|
||||||
|
# to reduce the size of the transition table and create bitmasks.
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
|
||||||
|
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
|
||||||
|
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||||
|
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
|
||||||
|
|
||||||
|
# The second part is a transition table that maps a combination
|
||||||
|
# of a state of the automaton and a character class to a state.
|
||||||
|
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
|
||||||
|
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
|
||||||
|
12,36,12,12,12,12,12,12,12,12,12,12, ]
|
||||||
|
|
||||||
|
def _decode(state, codep, ch):
|
||||||
|
tp = _UTF8D[ch]
|
||||||
|
|
||||||
|
codep = (ch & 0x3f ) | (codep << 6) if (state != UTF8_ACCEPT) else (0xff >> tp) & (ch)
|
||||||
|
state = _UTF8D[256 + state + tp]
|
||||||
|
|
||||||
|
return state, codep;
|
||||||
|
|
||||||
|
def validate_utf8(utfbytes):
|
||||||
|
"""
|
||||||
|
validate utf8 byte string.
|
||||||
|
utfbytes: utf byte string to check.
|
||||||
|
return value: if valid utf8 string, return true. Otherwise, return false.
|
||||||
|
"""
|
||||||
|
state = UTF8_ACCEPT
|
||||||
|
codep = 0
|
||||||
|
for i in utfbytes:
|
||||||
|
if six.PY2:
|
||||||
|
i = ord(i)
|
||||||
|
state, codep = _decode(state, codep, i)
|
||||||
|
if state == UTF8_REJECT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def extract_err_message(exception):
|
||||||
|
return getattr(exception, 'strerror', str(exception))
|
||||||
4966
lib/websocket/cacert.pem
Normal file
4966
lib/websocket/cacert.pem
Normal file
File diff suppressed because it is too large
Load Diff
0
lib/websocket/tests/__init__.py
Normal file
0
lib/websocket/tests/__init__.py
Normal file
6
lib/websocket/tests/data/header01.txt
Normal file
6
lib/websocket/tests/data/header01.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade
|
||||||
|
Upgrade: WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
||||||
6
lib/websocket/tests/data/header02.txt
Normal file
6
lib/websocket/tests/data/header02.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
HTTP/1.1 101 WebSocket Protocol Handshake
|
||||||
|
Connection: Upgrade
|
||||||
|
Upgrade WebSocket
|
||||||
|
Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0=
|
||||||
|
some_header: something
|
||||||
|
|
||||||
660
lib/websocket/tests/test_websocket.py
Normal file
660
lib/websocket/tests/test_websocket.py
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
|
||||||
|
import six
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import base64
|
||||||
|
import socket
|
||||||
|
try:
|
||||||
|
from ssl import SSLError
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for ssl none-support environment.
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
import unittest2 as unittest
|
||||||
|
else:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
from base64 import decodebytes as base64decode
|
||||||
|
else:
|
||||||
|
from base64 import decodestring as base64decode
|
||||||
|
|
||||||
|
|
||||||
|
# websocket-client
|
||||||
|
import websocket as ws
|
||||||
|
from websocket._handshake import _create_sec_websocket_key
|
||||||
|
from websocket._url import parse_url, get_proxy_info
|
||||||
|
from websocket._utils import validate_utf8
|
||||||
|
from websocket._handshake import _validate as _validate_header
|
||||||
|
from websocket._http import read_headers
|
||||||
|
|
||||||
|
|
||||||
|
# Skip test to access the internet.
|
||||||
|
TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1'
|
||||||
|
|
||||||
|
# Skip Secure WebSocket test.
|
||||||
|
TEST_SECURE_WS = True
|
||||||
|
TRACABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def create_mask_key(n):
|
||||||
|
return "abcd"
|
||||||
|
|
||||||
|
|
||||||
|
class SockMock(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.data = []
|
||||||
|
self.sent = []
|
||||||
|
|
||||||
|
def add_packet(self, data):
|
||||||
|
self.data.append(data)
|
||||||
|
|
||||||
|
def recv(self, bufsize):
|
||||||
|
if self.data:
|
||||||
|
e = self.data.pop(0)
|
||||||
|
if isinstance(e, Exception):
|
||||||
|
raise e
|
||||||
|
if len(e) > bufsize:
|
||||||
|
self.data.insert(0, e[bufsize:])
|
||||||
|
return e[:bufsize]
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
self.sent.append(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderSockMock(SockMock):
|
||||||
|
|
||||||
|
def __init__(self, fname):
|
||||||
|
SockMock.__init__(self)
|
||||||
|
path = os.path.join(os.path.dirname(__file__), fname)
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
self.add_packet(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ws.enableTrace(TRACABLE)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testDefaultTimeout(self):
|
||||||
|
self.assertEqual(ws.getdefaulttimeout(), None)
|
||||||
|
ws.setdefaulttimeout(10)
|
||||||
|
self.assertEqual(ws.getdefaulttimeout(), 10)
|
||||||
|
ws.setdefaulttimeout(None)
|
||||||
|
|
||||||
|
def testParseUrl(self):
|
||||||
|
p = parse_url("ws://www.example.com/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com/r/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080/")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://www.example.com:8080")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("wss://www.example.com:8080/r")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
p = parse_url("wss://www.example.com:8080/r?key=value")
|
||||||
|
self.assertEqual(p[0], "www.example.com")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r?key=value")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, parse_url, "http://www.example.com/r")
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
|
return
|
||||||
|
|
||||||
|
p = parse_url("ws://[2a03:4000:123:83::3]/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 80)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("ws://[2a03:4000:123:83::3]:8080/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], False)
|
||||||
|
|
||||||
|
p = parse_url("wss://[2a03:4000:123:83::3]/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 443)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
p = parse_url("wss://[2a03:4000:123:83::3]:8080/r")
|
||||||
|
self.assertEqual(p[0], "2a03:4000:123:83::3")
|
||||||
|
self.assertEqual(p[1], 8080)
|
||||||
|
self.assertEqual(p[2], "/r")
|
||||||
|
self.assertEqual(p[3], True)
|
||||||
|
|
||||||
|
def testWSKey(self):
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
self.assertTrue(key != 24)
|
||||||
|
self.assertTrue(six.u("¥n") not in key)
|
||||||
|
|
||||||
|
def testWsUtils(self):
|
||||||
|
key = "c6b8hTg4EeGb2gQMztV1/g=="
|
||||||
|
required_header = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
"sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0=",
|
||||||
|
}
|
||||||
|
self.assertEqual(_validate_header(required_header, key, None), (True, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["upgrade"] = "http"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["upgrade"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["connection"] = "something"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["connection"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-accept"] = "something"
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
del header["sec-websocket-accept"]
|
||||||
|
self.assertEqual(_validate_header(header, key, None), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-protocol"] = "sub1"
|
||||||
|
self.assertEqual(_validate_header(header, key, ["sub1", "sub2"]), (True, "sub1"))
|
||||||
|
self.assertEqual(_validate_header(header, key, ["sub2", "sub3"]), (False, None))
|
||||||
|
|
||||||
|
header = required_header.copy()
|
||||||
|
header["sec-websocket-protocol"] = "sUb1"
|
||||||
|
self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (True, "sub1"))
|
||||||
|
|
||||||
|
|
||||||
|
def testReadHeader(self):
|
||||||
|
status, header = read_headers(HeaderSockMock("data/header01.txt"))
|
||||||
|
self.assertEqual(status, 101)
|
||||||
|
self.assertEqual(header["connection"], "upgrade")
|
||||||
|
|
||||||
|
HeaderSockMock("data/header02.txt")
|
||||||
|
self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt"))
|
||||||
|
|
||||||
|
def testSend(self):
|
||||||
|
# TODO: add longer frame data
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.set_mask_key(create_mask_key)
|
||||||
|
s = sock.sock = HeaderSockMock("data/header01.txt")
|
||||||
|
sock.send("Hello")
|
||||||
|
self.assertEqual(s.sent[0], six.b("\x81\x85abcd)\x07\x0f\x08\x0e"))
|
||||||
|
|
||||||
|
sock.send("こんにちは")
|
||||||
|
self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
sock.send(u"こんにちは")
|
||||||
|
self.assertEqual(s.sent[1], six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc"))
|
||||||
|
|
||||||
|
sock.send("x" * 127)
|
||||||
|
|
||||||
|
def testRecv(self):
|
||||||
|
# TODO: add longer frame data
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
something = six.b("\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc")
|
||||||
|
s.add_packet(something)
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "こんにちは")
|
||||||
|
|
||||||
|
s.add_packet(six.b("\x81\x85abcd)\x07\x0f\x08\x0e"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Hello")
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testIter(self):
|
||||||
|
count = 2
|
||||||
|
for rsvp in ws.create_connection('ws://stream.meetup.com/2/rsvps'):
|
||||||
|
count -= 1
|
||||||
|
if count == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testNext(self):
|
||||||
|
sock = ws.create_connection('ws://stream.meetup.com/2/rsvps')
|
||||||
|
self.assertEqual(str, type(next(sock)))
|
||||||
|
|
||||||
|
def testInternalRecvStrict(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
s.add_packet(six.b("foo"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("bar"))
|
||||||
|
# s.add_packet(SSLError("The read operation timed out"))
|
||||||
|
s.add_packet(six.b("baz"))
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
data = sock.frame_buffer.recv_strict(9)
|
||||||
|
# if six.PY2:
|
||||||
|
# with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
# data = sock._recv_strict(9)
|
||||||
|
# else:
|
||||||
|
# with self.assertRaises(SSLError):
|
||||||
|
# data = sock._recv_strict(9)
|
||||||
|
data = sock.frame_buffer.recv_strict(9)
|
||||||
|
self.assertEqual(data, six.b("foobarbaz"))
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
data = sock.frame_buffer.recv_strict(1)
|
||||||
|
|
||||||
|
def testRecvTimeout(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
s.add_packet(six.b("\x81"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("\x8dabcd\x29\x07\x0f\x08\x0e"))
|
||||||
|
s.add_packet(socket.timeout())
|
||||||
|
s.add_packet(six.b("\x4e\x43\x33\x0e\x10\x0f\x00\x40"))
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
data = sock.recv()
|
||||||
|
with self.assertRaises(ws.WebSocketTimeoutException):
|
||||||
|
data = sock.recv()
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Hello, World!")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
data = sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithSimpleFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Brevity is the soul of wit")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithFireEventOfFragmentation(self):
|
||||||
|
sock = ws.WebSocket(fire_cont_frame=True)
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("Brevity is "))
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("Brevity is "))
|
||||||
|
_, data = sock.recv_data()
|
||||||
|
self.assertEqual(data, six.b("the soul of wit"))
|
||||||
|
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="Brevity is "
|
||||||
|
s.add_packet(six.b("\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C"))
|
||||||
|
|
||||||
|
with self.assertRaises(ws.WebSocketException):
|
||||||
|
sock.recv_data()
|
||||||
|
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testClose(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.sock = SockMock()
|
||||||
|
sock.connected = True
|
||||||
|
sock.close()
|
||||||
|
self.assertEqual(sock.connected, False)
|
||||||
|
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
sock.connected = True
|
||||||
|
s.add_packet(six.b('\x88\x80\x17\x98p\x84'))
|
||||||
|
sock.recv()
|
||||||
|
self.assertEqual(sock.connected, False)
|
||||||
|
|
||||||
|
def testRecvContFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="the soul of wit"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17"))
|
||||||
|
self.assertRaises(ws.WebSocketException, sock.recv)
|
||||||
|
|
||||||
|
def testRecvWithProlongedFragmentation(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Once more unto the breach, "
|
||||||
|
s.add_packet(six.b("\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15" \
|
||||||
|
"\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC"))
|
||||||
|
# OPCODE=CONT, FIN=0, MSG="dear friends, "
|
||||||
|
s.add_packet(six.b("\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07" \
|
||||||
|
"\x17MB"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="once more"
|
||||||
|
s.add_packet(six.b("\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(
|
||||||
|
data,
|
||||||
|
"Once more unto the breach, dear friends, once more")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
|
||||||
|
def testRecvWithFragmentationAndControlFrame(self):
|
||||||
|
sock = ws.WebSocket()
|
||||||
|
sock.set_mask_key(create_mask_key)
|
||||||
|
s = sock.sock = SockMock()
|
||||||
|
# OPCODE=TEXT, FIN=0, MSG="Too much "
|
||||||
|
s.add_packet(six.b("\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA"))
|
||||||
|
# OPCODE=PING, FIN=1, MSG="Please PONG this"
|
||||||
|
s.add_packet(six.b("\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17"))
|
||||||
|
# OPCODE=CONT, FIN=1, MSG="of a good thing"
|
||||||
|
s.add_packet(six.b("\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c" \
|
||||||
|
"\x08\x0c\x04"))
|
||||||
|
data = sock.recv()
|
||||||
|
self.assertEqual(data, "Too much of a good thing")
|
||||||
|
with self.assertRaises(ws.WebSocketConnectionClosedException):
|
||||||
|
sock.recv()
|
||||||
|
self.assertEqual(
|
||||||
|
s.sent[0],
|
||||||
|
six.b("\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17"))
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testWebSocket(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
|
||||||
|
s.send(u"こにゃにゃちは、世界")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "こにゃにゃちは、世界")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testPingPong(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.ping("Hello")
|
||||||
|
s.pong("Hi")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
@unittest.skipUnless(TEST_SECURE_WS, "wss://echo.websocket.org doesn't work well.")
|
||||||
|
def testSecureWebSocket(self):
|
||||||
|
if 1:
|
||||||
|
import ssl
|
||||||
|
s = ws.create_connection("wss://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
self.assertTrue(isinstance(s.sock, ssl.SSLSocket))
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
s.send(u"こにゃにゃちは、世界")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "こにゃにゃちは、世界")
|
||||||
|
s.close()
|
||||||
|
#except:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testWebSocketWihtCustomHeader(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/",
|
||||||
|
headers={"User-Agent": "PythonWebsocketClient"})
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.send("Hello, World")
|
||||||
|
result = s.recv()
|
||||||
|
self.assertEqual(result, "Hello, World")
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testAfterClose(self):
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org/")
|
||||||
|
self.assertNotEqual(s, None)
|
||||||
|
s.close()
|
||||||
|
self.assertRaises(ws.WebSocketConnectionClosedException, s.send, "Hello")
|
||||||
|
self.assertRaises(ws.WebSocketConnectionClosedException, s.recv)
|
||||||
|
|
||||||
|
def testUUID4(self):
|
||||||
|
""" WebSocket key should be a UUID4.
|
||||||
|
"""
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
u = uuid.UUID(bytes=base64decode(key.encode("utf-8")))
|
||||||
|
self.assertEqual(4, u.version)
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketAppTest(unittest.TestCase):
|
||||||
|
|
||||||
|
class NotSetYet(object):
|
||||||
|
""" A marker class for signalling that a value hasn't been set yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
ws.enableTrace(TRACABLE)
|
||||||
|
|
||||||
|
WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet()
|
||||||
|
WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet()
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testKeepRunning(self):
|
||||||
|
""" A WebSocketApp should keep running as long as its self.keep_running
|
||||||
|
is not False (in the boolean context).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_open(self, *args, **kwargs):
|
||||||
|
""" Set the keep_running flag for later inspection and immediately
|
||||||
|
close the connection.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.keep_running_open = self.keep_running
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def on_close(self, *args, **kwargs):
|
||||||
|
""" Set the keep_running flag for the test to use.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.keep_running_close = self.keep_running
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, on_close=on_close)
|
||||||
|
app.run_forever()
|
||||||
|
|
||||||
|
self.assertFalse(isinstance(WebSocketAppTest.keep_running_open,
|
||||||
|
WebSocketAppTest.NotSetYet))
|
||||||
|
|
||||||
|
self.assertFalse(isinstance(WebSocketAppTest.keep_running_close,
|
||||||
|
WebSocketAppTest.NotSetYet))
|
||||||
|
|
||||||
|
self.assertEqual(True, WebSocketAppTest.keep_running_open)
|
||||||
|
self.assertEqual(False, WebSocketAppTest.keep_running_close)
|
||||||
|
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSockMaskKey(self):
|
||||||
|
""" A WebSocketApp should forward the received mask_key function down
|
||||||
|
to the actual socket.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def my_mask_key_func():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_open(self, *args, **kwargs):
|
||||||
|
""" Set the value so the test can use it later on and immediately
|
||||||
|
close the connection.
|
||||||
|
"""
|
||||||
|
WebSocketAppTest.get_mask_key_id = id(self.get_mask_key)
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
app = ws.WebSocketApp('ws://echo.websocket.org/', on_open=on_open, get_mask_key=my_mask_key_func)
|
||||||
|
app.run_forever()
|
||||||
|
|
||||||
|
# Note: We can't use 'is' for comparing the functions directly, need to use 'id'.
|
||||||
|
self.assertEqual(WebSocketAppTest.get_mask_key_id, id(my_mask_key_func))
|
||||||
|
|
||||||
|
|
||||||
|
class SockOptTest(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
|
||||||
|
def testSockOpt(self):
|
||||||
|
sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),)
|
||||||
|
s = ws.create_connection("ws://echo.websocket.org", sockopt=sockopt)
|
||||||
|
self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
class UtilsTest(unittest.TestCase):
|
||||||
|
def testUtf8Validator(self):
|
||||||
|
state = validate_utf8(six.b('\xf0\x90\x80\x80'))
|
||||||
|
self.assertEqual(state, True)
|
||||||
|
state = validate_utf8(six.b('\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited'))
|
||||||
|
self.assertEqual(state, False)
|
||||||
|
state = validate_utf8(six.b(''))
|
||||||
|
self.assertEqual(state, True)
|
||||||
|
|
||||||
|
class ProxyInfoTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.http_proxy = os.environ.get("http_proxy", None)
|
||||||
|
self.https_proxy = os.environ.get("https_proxy", None)
|
||||||
|
if "http_proxy" in os.environ:
|
||||||
|
del os.environ["http_proxy"]
|
||||||
|
if "https_proxy" in os.environ:
|
||||||
|
del os.environ["https_proxy"]
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if self.http_proxy:
|
||||||
|
os.environ["http_proxy"] = self.http_proxy
|
||||||
|
elif "http_proxy" in os.environ:
|
||||||
|
del os.environ["http_proxy"]
|
||||||
|
|
||||||
|
if self.https_proxy:
|
||||||
|
os.environ["https_proxy"] = self.https_proxy
|
||||||
|
elif "https_proxy" in os.environ:
|
||||||
|
del os.environ["https_proxy"]
|
||||||
|
|
||||||
|
|
||||||
|
def testProxyFromArgs(self):
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost"), ("localhost", 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost"), ("localhost", 0, None))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_auth=("a", "b")),
|
||||||
|
("localhost", 0, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_auth=("a", "b")),
|
||||||
|
("localhost", 0, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["example.com"], proxy_auth=("a", "b")),
|
||||||
|
("localhost", 3128, ("a", "b")))
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True, proxy_host="localhost", proxy_port=3128, no_proxy=["echo.websocket.org"], proxy_auth=("a", "b")),
|
||||||
|
(None, 0, None))
|
||||||
|
|
||||||
|
|
||||||
|
def testProxyFromEnv(self):
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, None))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, None))
|
||||||
|
os.environ["http_proxy"] = "http://localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, None))
|
||||||
|
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", False), ("localhost", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), ("localhost2", 3128, ("a", "b")))
|
||||||
|
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com"
|
||||||
|
self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b")))
|
||||||
|
os.environ["http_proxy"] = "http://a:b@localhost:3128/"
|
||||||
|
os.environ["https_proxy"] = "http://a:b@localhost2:3128/"
|
||||||
|
os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.org"
|
||||||
|
self.assertEqual(get_proxy_info("echo.websocket.org", True), (None, 0, None))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -17,16 +17,21 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import webbrowser
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import cherrypy
|
import cherrypy
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
# Some cut down versions of Python may not include this module and it's not critical for us
|
||||||
|
try:
|
||||||
|
import webbrowser
|
||||||
|
no_browser = False
|
||||||
|
except ImportError:
|
||||||
|
no_browser = True
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
|
|
||||||
from plexpy import versioncheck, logger, monitor, plextv
|
from plexpy import versioncheck, logger, activity_pinger, plextv, pmsconnect
|
||||||
import plexpy.config
|
import plexpy.config
|
||||||
|
|
||||||
PROG_DIR = None
|
PROG_DIR = None
|
||||||
@@ -66,6 +71,7 @@ COMMITS_BEHIND = None
|
|||||||
|
|
||||||
UMASK = None
|
UMASK = None
|
||||||
|
|
||||||
|
POLLING_FAILOVER = False
|
||||||
|
|
||||||
def initialize(config_file):
|
def initialize(config_file):
|
||||||
with INIT_LOCK:
|
with INIT_LOCK:
|
||||||
@@ -75,6 +81,7 @@ def initialize(config_file):
|
|||||||
global CURRENT_VERSION
|
global CURRENT_VERSION
|
||||||
global LATEST_VERSION
|
global LATEST_VERSION
|
||||||
global UMASK
|
global UMASK
|
||||||
|
global POLLING_FAILOVER
|
||||||
|
|
||||||
CONFIG = plexpy.config.Config(config_file)
|
CONFIG = plexpy.config.Config(config_file)
|
||||||
|
|
||||||
@@ -162,6 +169,7 @@ def initialize(config_file):
|
|||||||
# Get the real PMS urls for SSL and remote access
|
# Get the real PMS urls for SSL and remote access
|
||||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||||
plextv.get_real_pms_url()
|
plextv.get_real_pms_url()
|
||||||
|
pmsconnect.get_server_friendly_name()
|
||||||
|
|
||||||
# Refresh the users list on startup
|
# Refresh the users list on startup
|
||||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
||||||
@@ -228,6 +236,7 @@ def daemonize():
|
|||||||
|
|
||||||
|
|
||||||
def launch_browser(host, port, root):
|
def launch_browser(host, port, root):
|
||||||
|
if not no_browser:
|
||||||
if host == '0.0.0.0':
|
if host == '0.0.0.0':
|
||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
|
|
||||||
@@ -272,8 +281,29 @@ def initialize_scheduler():
|
|||||||
seconds = 0
|
seconds = 0
|
||||||
|
|
||||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs', hours=12, minutes=0, seconds=0)
|
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs',
|
||||||
schedule_job(monitor.check_active_sessions, 'Check for active sessions', hours=0, minutes=0, seconds=seconds)
|
hours=12, minutes=0, seconds=0)
|
||||||
|
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex Server Name',
|
||||||
|
hours=12, minutes=0, seconds=0)
|
||||||
|
|
||||||
|
if CONFIG.NOTIFY_RECENTLY_ADDED:
|
||||||
|
schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||||
|
hours=0, minutes=0, seconds=seconds)
|
||||||
|
else:
|
||||||
|
schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||||
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
|
if CONFIG.MONITOR_REMOTE_ACCESS:
|
||||||
|
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||||
|
hours=0, minutes=0, seconds=seconds)
|
||||||
|
else:
|
||||||
|
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||||
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
|
# If we're not using websockets then fall back to polling
|
||||||
|
if not CONFIG.MONITORING_USE_WEBSOCKET or POLLING_FAILOVER:
|
||||||
|
schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
|
||||||
|
hours=0, minutes=0, seconds=seconds)
|
||||||
|
|
||||||
# Refresh the users list
|
# Refresh the users list
|
||||||
if CONFIG.REFRESH_USERS_INTERVAL:
|
if CONFIG.REFRESH_USERS_INTERVAL:
|
||||||
@@ -348,12 +378,13 @@ def dbcheck():
|
|||||||
'bitrate INTEGER, video_resolution TEXT, video_framerate TEXT, aspect_ratio TEXT, '
|
'bitrate INTEGER, video_resolution TEXT, video_framerate TEXT, aspect_ratio TEXT, '
|
||||||
'audio_channels INTEGER, transcode_protocol TEXT, transcode_container TEXT, '
|
'audio_channels INTEGER, transcode_protocol TEXT, transcode_container TEXT, '
|
||||||
'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,'
|
'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,'
|
||||||
'transcode_width INTEGER, transcode_height INTEGER)'
|
'transcode_width INTEGER, transcode_height INTEGER, buffer_count INTEGER DEFAULT 0, '
|
||||||
|
'buffer_last_triggered INTEGER, last_paused INTEGER)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# session_history table :: This is a history table which logs essential stream details
|
# session_history table :: This is a history table which logs essential stream details
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS session_history (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS session_history (id INTEGER PRIMARY KEY AUTOINCREMENT, reference_id INTEGER, '
|
||||||
'started INTEGER, stopped INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
|
'started INTEGER, stopped INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
|
||||||
'ip_address TEXT, paused_counter INTEGER DEFAULT 0, player TEXT, platform TEXT, machine_id TEXT, '
|
'ip_address TEXT, paused_counter INTEGER DEFAULT 0, player TEXT, platform TEXT, machine_id TEXT, '
|
||||||
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, media_type TEXT, view_offset INTEGER DEFAULT 0)'
|
'parent_rating_key INTEGER, grandparent_rating_key INTEGER, media_type TEXT, view_offset INTEGER DEFAULT 0)'
|
||||||
@@ -376,7 +407,7 @@ def dbcheck():
|
|||||||
'title TEXT, parent_title TEXT, grandparent_title TEXT, full_title TEXT, media_index INTEGER, '
|
'title TEXT, parent_title TEXT, grandparent_title TEXT, full_title TEXT, media_index INTEGER, '
|
||||||
'parent_media_index INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, art TEXT, media_type TEXT, '
|
'parent_media_index INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, art TEXT, media_type TEXT, '
|
||||||
'year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, last_viewed_at INTEGER, '
|
'year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, last_viewed_at INTEGER, '
|
||||||
'content_rating TEXT, summary TEXT, rating TEXT, duration INTEGER DEFAULT 0, guid TEXT, '
|
'content_rating TEXT, summary TEXT, tagline TEXT, rating TEXT, duration INTEGER DEFAULT 0, guid TEXT, '
|
||||||
'directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT)'
|
'directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT)'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
@@ -385,8 +416,9 @@ def dbcheck():
|
|||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL UNIQUE, '
|
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL UNIQUE, '
|
||||||
'friendly_name TEXT, thumb TEXT, email TEXT, is_home_user INTEGER DEFAULT NULL, '
|
'friendly_name TEXT, thumb TEXT, email TEXT, custom_avatar_url TEXT, is_home_user INTEGER DEFAULT NULL, '
|
||||||
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1)'
|
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
|
||||||
|
'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Upgrade sessions table from earlier versions
|
# Upgrade sessions table from earlier versions
|
||||||
@@ -515,29 +547,140 @@ def dbcheck():
|
|||||||
'ALTER TABLE sessions ADD COLUMN transcode_height INTEGER'
|
'ALTER TABLE sessions ADD COLUMN transcode_height INTEGER'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Upgrade sessions table from earlier versions
|
# Upgrade session_history_metadata table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT full_title from session_history_metadata')
|
c_db.execute('SELECT full_title from session_history_metadata')
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
logger.debug(u"Altering database. Updating database table sessions.")
|
logger.debug(u"Altering database. Updating database table session_history_metadata.")
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'ALTER TABLE session_history_metadata ADD COLUMN full_title TEXT'
|
'ALTER TABLE session_history_metadata ADD COLUMN full_title TEXT'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Upgrade session_history_metadata table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT tagline from session_history_metadata')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table session_history_metadata.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE session_history_metadata ADD COLUMN tagline TEXT'
|
||||||
|
)
|
||||||
|
|
||||||
# notify_log table :: This is a table which logs notifications sent
|
# notify_log table :: This is a table which logs notifications sent
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||||
'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
|
'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
|
||||||
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER)'
|
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER, '
|
||||||
|
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER, on_created INTEGER)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade users table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT do_notify from users')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table users.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE users ADD COLUMN do_notify INTEGER DEFAULT 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade users table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT keep_history from users')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table users.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE users ADD COLUMN keep_history INTEGER DEFAULT 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade notify_log table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT on_pause from notify_log')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table notify_log.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE notify_log ADD COLUMN on_pause INTEGER'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE notify_log ADD COLUMN on_resume INTEGER'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE notify_log ADD COLUMN on_buffer INTEGER'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade notify_log table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT on_created from notify_log')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table notify_log.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE notify_log ADD COLUMN on_created INTEGER'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Upgrade sessions table from earlier versions
|
# Upgrade sessions table from earlier versions
|
||||||
try:
|
try:
|
||||||
c_db.execute('SELECT do_notify from users')
|
c_db.execute('SELECT buffer_count from sessions')
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
logger.debug(u"Altering database. Updating database table sessions.")
|
logger.debug(u"Altering database. Updating database table sessions.")
|
||||||
c_db.execute(
|
c_db.execute(
|
||||||
'ALTER TABLE users ADD COLUMN do_notify INTEGER DEFAULT 1'
|
'ALTER TABLE sessions ADD COLUMN buffer_count INTEGER DEFAULT 0'
|
||||||
|
)
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE sessions ADD COLUMN buffer_last_triggered INTEGER'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade users table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT custom_avatar_url from users')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table users.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE users ADD COLUMN custom_avatar_url TEXT'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade sessions table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT last_paused from sessions')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table sessions.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE sessions ADD COLUMN last_paused INTEGER'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add "Local" user to database as default unauthenticated user.
|
||||||
|
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
|
||||||
|
if not result.fetchone():
|
||||||
|
logger.debug(u'User "Local" does not exist. Adding user.')
|
||||||
|
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
|
||||||
|
|
||||||
|
# Upgrade session_history table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT reference_id from session_history')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table session_history.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE session_history ADD COLUMN reference_id INTEGER DEFAULT 0'
|
||||||
|
)
|
||||||
|
# Set reference_id to the first row where (user_id = previous row, rating_key != previous row) and user_id = user_id
|
||||||
|
c_db.execute(
|
||||||
|
'UPDATE session_history ' \
|
||||||
|
'SET reference_id = (SELECT (CASE \
|
||||||
|
WHEN (SELECT MIN(id) FROM session_history WHERE id > ( \
|
||||||
|
SELECT MAX(id) FROM session_history \
|
||||||
|
WHERE (user_id = t1.user_id AND rating_key <> t1.rating_key AND id < t1.id)) AND user_id = t1.user_id) IS NULL \
|
||||||
|
THEN (SELECT MIN(id) FROM session_history WHERE (user_id = t1.user_id)) \
|
||||||
|
ELSE (SELECT MIN(id) FROM session_history WHERE id > ( \
|
||||||
|
SELECT MAX(id) FROM session_history \
|
||||||
|
WHERE (user_id = t1.user_id AND rating_key <> t1.rating_key AND id < t1.id)) AND user_id = t1.user_id) END) ' \
|
||||||
|
'FROM session_history AS t1 ' \
|
||||||
|
'WHERE t1.id = session_history.id) '
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upgrade users table from earlier versions
|
||||||
|
try:
|
||||||
|
c_db.execute('SELECT deleted_user from users')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
logger.debug(u"Altering database. Updating database table users.")
|
||||||
|
c_db.execute(
|
||||||
|
'ALTER TABLE users ADD COLUMN deleted_user INTEGER DEFAULT 0'
|
||||||
)
|
)
|
||||||
|
|
||||||
conn_db.commit()
|
conn_db.commit()
|
||||||
@@ -547,10 +690,6 @@ def shutdown(restart=False, update=False):
|
|||||||
cherrypy.engine.exit()
|
cherrypy.engine.exit()
|
||||||
SCHED.shutdown(wait=False)
|
SCHED.shutdown(wait=False)
|
||||||
|
|
||||||
# Clear any sessions in the db - Not sure yet if we should do this. More testing required
|
|
||||||
# logger.debug(u'Clearing Plex sessions.')
|
|
||||||
# monitor.drop_session_db()
|
|
||||||
|
|
||||||
CONFIG.write()
|
CONFIG.write()
|
||||||
|
|
||||||
if not restart and not update:
|
if not restart and not update:
|
||||||
|
|||||||
265
plexpy/activity_handler.py
Normal file
265
plexpy/activity_handler.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PlexPy is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import time
|
||||||
|
import plexpy
|
||||||
|
|
||||||
|
from plexpy import logger, pmsconnect, activity_processor, threading, notification_handler
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityHandler(object):
|
||||||
|
|
||||||
|
def __init__(self, timeline):
|
||||||
|
self.timeline = timeline
|
||||||
|
# print timeline
|
||||||
|
|
||||||
|
def is_valid_session(self):
|
||||||
|
if 'sessionKey' in self.timeline:
|
||||||
|
if str(self.timeline['sessionKey']).isdigit():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_session_key(self):
|
||||||
|
if self.is_valid_session():
|
||||||
|
return int(self.timeline['sessionKey'])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_live_session(self):
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
session_list = pms_connect.get_current_activity()
|
||||||
|
|
||||||
|
for session in session_list['sessions']:
|
||||||
|
if int(session['session_key']) == self.get_session_key():
|
||||||
|
return session
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_db_session(self):
|
||||||
|
# Update our session temp table values
|
||||||
|
monitor_proc = activity_processor.ActivityProcessor()
|
||||||
|
monitor_proc.write_session(session=self.get_live_session(), notify=False)
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
if self.is_valid_session():
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s has started." % str(self.get_session_key()))
|
||||||
|
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=self.get_live_session(), notify_action='play')).start()
|
||||||
|
|
||||||
|
# Write the new session to our temp session table
|
||||||
|
self.update_db_session()
|
||||||
|
|
||||||
|
def on_stop(self, force_stop=False):
|
||||||
|
if self.is_valid_session():
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s has stopped." % str(self.get_session_key()))
|
||||||
|
|
||||||
|
# Set the session last_paused timestamp
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None)
|
||||||
|
|
||||||
|
# Update the session state and viewOffset
|
||||||
|
# Set force_stop to true to disable the state set
|
||||||
|
if not force_stop:
|
||||||
|
ap.set_session_state(session_key=self.get_session_key(),
|
||||||
|
state=self.timeline['state'],
|
||||||
|
view_offset=self.timeline['viewOffset'])
|
||||||
|
|
||||||
|
# Retrieve the session data from our temp table
|
||||||
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=db_session, notify_action='stop')).start()
|
||||||
|
|
||||||
|
# Write it to the history table
|
||||||
|
monitor_proc = activity_processor.ActivityProcessor()
|
||||||
|
monitor_proc.write_session_history(session=db_session)
|
||||||
|
|
||||||
|
# Remove the session from our temp session table
|
||||||
|
ap.delete_session(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
def on_pause(self):
|
||||||
|
if self.is_valid_session():
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s has been paused." % str(self.get_session_key()))
|
||||||
|
|
||||||
|
# Set the session last_paused timestamp
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=int(time.time()))
|
||||||
|
|
||||||
|
# Update the session state and viewOffset
|
||||||
|
ap.set_session_state(session_key=self.get_session_key(),
|
||||||
|
state=self.timeline['state'],
|
||||||
|
view_offset=self.timeline['viewOffset'])
|
||||||
|
|
||||||
|
# Retrieve the session data from our temp table
|
||||||
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=db_session, notify_action='pause')).start()
|
||||||
|
|
||||||
|
def on_resume(self):
|
||||||
|
if self.is_valid_session():
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s has been resumed." % str(self.get_session_key()))
|
||||||
|
|
||||||
|
# Set the session last_paused timestamp
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
ap.set_session_last_paused(session_key=self.get_session_key(), timestamp=None)
|
||||||
|
|
||||||
|
# Update the session state and viewOffset
|
||||||
|
ap.set_session_state(session_key=self.get_session_key(),
|
||||||
|
state=self.timeline['state'],
|
||||||
|
view_offset=self.timeline['viewOffset'])
|
||||||
|
|
||||||
|
# Retrieve the session data from our temp table
|
||||||
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=db_session, notify_action='resume')).start()
|
||||||
|
|
||||||
|
def on_buffer(self):
|
||||||
|
if self.is_valid_session():
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s is buffering." % self.get_session_key())
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
db_stream = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
# Increment our buffer count
|
||||||
|
ap.increment_session_buffer_count(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
# Get our current buffer count
|
||||||
|
current_buffer_count = ap.get_session_buffer_count(self.get_session_key())
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s buffer count is %s." %
|
||||||
|
(self.get_session_key(), current_buffer_count))
|
||||||
|
|
||||||
|
# Get our last triggered time
|
||||||
|
buffer_last_triggered = ap.get_session_buffer_trigger_time(self.get_session_key())
|
||||||
|
|
||||||
|
time_since_last_trigger = 0
|
||||||
|
if buffer_last_triggered:
|
||||||
|
logger.debug(u"PlexPy ActivityHandler :: Session %s buffer last triggered at %s." %
|
||||||
|
(self.get_session_key(), buffer_last_triggered))
|
||||||
|
time_since_last_trigger = int(time.time()) - int(buffer_last_triggered)
|
||||||
|
|
||||||
|
if current_buffer_count >= plexpy.CONFIG.BUFFER_THRESHOLD and time_since_last_trigger == 0 or \
|
||||||
|
time_since_last_trigger >= plexpy.CONFIG.BUFFER_WAIT:
|
||||||
|
ap.set_session_buffer_trigger_time(session_key=self.get_session_key())
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=db_stream, notify_action='buffer')).start()
|
||||||
|
|
||||||
|
# This function receives events from our websocket connection
|
||||||
|
def process(self):
|
||||||
|
if self.is_valid_session():
|
||||||
|
from plexpy import helpers
|
||||||
|
|
||||||
|
ap = activity_processor.ActivityProcessor()
|
||||||
|
db_session = ap.get_session_by_key(session_key=self.get_session_key())
|
||||||
|
|
||||||
|
this_state = self.timeline['state']
|
||||||
|
this_key = str(self.timeline['ratingKey'])
|
||||||
|
|
||||||
|
# If we already have this session in the temp table, check for state changes
|
||||||
|
if db_session:
|
||||||
|
last_state = db_session['state']
|
||||||
|
last_key = str(db_session['rating_key'])
|
||||||
|
|
||||||
|
# Make sure the same item is being played
|
||||||
|
if this_key == last_key:
|
||||||
|
# Update the session state and viewOffset
|
||||||
|
if this_state == 'playing':
|
||||||
|
ap.set_session_state(session_key=self.get_session_key(),
|
||||||
|
state=this_state,
|
||||||
|
view_offset=self.timeline['viewOffset'])
|
||||||
|
# Start our state checks
|
||||||
|
if this_state != last_state:
|
||||||
|
if this_state == 'paused':
|
||||||
|
self.on_pause()
|
||||||
|
elif last_state == 'paused' and this_state == 'playing':
|
||||||
|
self.on_resume()
|
||||||
|
elif this_state == 'stopped':
|
||||||
|
self.on_stop()
|
||||||
|
elif this_state == 'buffering':
|
||||||
|
self.on_buffer()
|
||||||
|
# If a client doesn't register stop events (I'm looking at you PHT!) check if the ratingKey has changed
|
||||||
|
else:
|
||||||
|
# Manually stop and start
|
||||||
|
# Set force_stop so that we don't overwrite our last viewOffset
|
||||||
|
self.on_stop(force_stop=True)
|
||||||
|
self.on_start()
|
||||||
|
|
||||||
|
# Monitor if the stream has reached the watch percentage for notifications
|
||||||
|
# The only purpose of this is for notifications
|
||||||
|
progress_percent = helpers.get_percent(self.timeline['viewOffset'], db_session['duration'])
|
||||||
|
if progress_percent >= plexpy.CONFIG.NOTIFY_WATCHED_PERCENT and this_state != 'buffering':
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=db_session, notify_action='watched')).start()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We don't have this session in our table yet, start a new one.
|
||||||
|
if this_state != 'buffering':
|
||||||
|
self.on_start()
|
||||||
|
|
||||||
|
class TimelineHandler(object):
|
||||||
|
|
||||||
|
def __init__(self, timeline):
|
||||||
|
self.timeline = timeline
|
||||||
|
#logger.debug(timeline)
|
||||||
|
|
||||||
|
def is_item(self):
|
||||||
|
if 'itemID' in self.timeline:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_rating_key(self):
|
||||||
|
if self.is_item():
|
||||||
|
return int(self.timeline['itemID'])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_metadata(self):
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
metadata_list = pms_connect.get_metadata_details(self.get_rating_key())
|
||||||
|
|
||||||
|
if metadata_list:
|
||||||
|
return metadata_list['metadata']
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_created(self):
|
||||||
|
if self.is_item():
|
||||||
|
logger.debug(u"PlexPy TimelineHandler :: Library item %s has been added to Plex." % str(self.get_rating_key()))
|
||||||
|
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify_timeline,
|
||||||
|
kwargs=dict(timeline_data=self.get_metadata(), notify_action='created')).start()
|
||||||
|
|
||||||
|
# This function receives events from our websocket connection
|
||||||
|
def process(self):
|
||||||
|
if self.is_item():
|
||||||
|
|
||||||
|
this_state = self.timeline['state']
|
||||||
|
this_type = self.timeline['type']
|
||||||
|
this_metadataState = self.timeline.get('metadataState', None)
|
||||||
|
this_mediaState = self.timeline.get('mediaState', None)
|
||||||
|
|
||||||
|
# state: 5: done processing metadata
|
||||||
|
# type: 1: movie, 2: tv show, 4: episode, 8: artist, 10: track
|
||||||
|
types = [1, 2, 4, 8, 10]
|
||||||
|
if this_state == 5 and this_type in types and this_metadataState == None and this_mediaState == None:
|
||||||
|
self.on_created()
|
||||||
274
plexpy/activity_pinger.py
Normal file
274
plexpy/activity_pinger.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PlexPy is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from plexpy import logger, pmsconnect, plextv, notification_handler, database, helpers, activity_processor
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import plexpy
|
||||||
|
import time
|
||||||
|
|
||||||
|
monitor_lock = threading.Lock()
|
||||||
|
ext_ping_count = 0
|
||||||
|
int_ping_count = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check_active_sessions(ws_request=False):
|
||||||
|
|
||||||
|
with monitor_lock:
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
session_list = pms_connect.get_current_activity()
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
monitor_process = activity_processor.ActivityProcessor()
|
||||||
|
# logger.debug(u"PlexPy Monitor :: Checking for active streams.")
|
||||||
|
|
||||||
|
global int_ping_count
|
||||||
|
|
||||||
|
if session_list:
|
||||||
|
int_ping_count = 0
|
||||||
|
|
||||||
|
media_container = session_list['sessions']
|
||||||
|
|
||||||
|
# Check our temp table for what we must do with the new streams
|
||||||
|
db_streams = monitor_db.select('SELECT started, session_key, rating_key, media_type, title, parent_title, '
|
||||||
|
'grandparent_title, user_id, user, friendly_name, ip_address, player, '
|
||||||
|
'platform, machine_id, parent_rating_key, grandparent_rating_key, state, '
|
||||||
|
'view_offset, duration, video_decision, audio_decision, width, height, '
|
||||||
|
'container, video_codec, audio_codec, bitrate, video_resolution, '
|
||||||
|
'video_framerate, aspect_ratio, audio_channels, transcode_protocol, '
|
||||||
|
'transcode_container, transcode_video_codec, transcode_audio_codec, '
|
||||||
|
'transcode_audio_channels, transcode_width, transcode_height, '
|
||||||
|
'paused_counter, last_paused '
|
||||||
|
'FROM sessions')
|
||||||
|
for stream in db_streams:
|
||||||
|
if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key'])
|
||||||
|
for d in media_container):
|
||||||
|
# The user's session is still active
|
||||||
|
for session in media_container:
|
||||||
|
if session['session_key'] == str(stream['session_key']) and \
|
||||||
|
session['rating_key'] == str(stream['rating_key']):
|
||||||
|
# The user is still playing the same media item
|
||||||
|
# Here we can check the play states
|
||||||
|
if session['state'] != stream['state']:
|
||||||
|
if session['state'] == 'paused':
|
||||||
|
# Push any notifications -
|
||||||
|
# Push it on it's own thread so we don't hold up our db actions
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='pause')).start()
|
||||||
|
|
||||||
|
if session['state'] == 'playing' and stream['state'] == 'paused':
|
||||||
|
# Push any notifications -
|
||||||
|
# Push it on it's own thread so we don't hold up our db actions
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='resume')).start()
|
||||||
|
|
||||||
|
if stream['state'] == 'paused' and not ws_request:
|
||||||
|
# The stream is still paused so we need to increment the paused_counter
|
||||||
|
# Using the set config parameter as the interval, probably not the most accurate but
|
||||||
|
# it will have to do for now. If it's a websocket request don't use this method.
|
||||||
|
paused_counter = int(stream['paused_counter']) + plexpy.CONFIG.MONITORING_INTERVAL
|
||||||
|
monitor_db.action('UPDATE sessions SET paused_counter = ? '
|
||||||
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
|
[paused_counter, stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
|
if session['state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0:
|
||||||
|
# The stream is buffering so we need to increment the buffer_count
|
||||||
|
# We're going just increment on every monitor ping,
|
||||||
|
# would be difficult to keep track otherwise
|
||||||
|
monitor_db.action('UPDATE sessions SET buffer_count = buffer_count + 1 '
|
||||||
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
|
# Check the current buffer count and last buffer to determine if we should notify
|
||||||
|
buffer_values = monitor_db.select('SELECT buffer_count, buffer_last_triggered '
|
||||||
|
'FROM sessions '
|
||||||
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
|
if buffer_values[0]['buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD:
|
||||||
|
# Push any notifications -
|
||||||
|
# Push it on it's own thread so we don't hold up our db actions
|
||||||
|
# Our first buffer notification
|
||||||
|
if buffer_values[0]['buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD:
|
||||||
|
logger.info(u"PlexPy Monitor :: User '%s' has triggered a buffer warning."
|
||||||
|
% stream['user'])
|
||||||
|
# Set the buffer trigger time
|
||||||
|
monitor_db.action('UPDATE sessions '
|
||||||
|
'SET buffer_last_triggered = strftime("%s","now") '
|
||||||
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||||
|
else:
|
||||||
|
# Subsequent buffer notifications after wait time
|
||||||
|
if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \
|
||||||
|
plexpy.CONFIG.BUFFER_WAIT:
|
||||||
|
logger.info(u"PlexPy Monitor :: User '%s' has triggered multiple buffer warnings."
|
||||||
|
% stream['user'])
|
||||||
|
# Set the buffer trigger time
|
||||||
|
monitor_db.action('UPDATE sessions '
|
||||||
|
'SET buffer_last_triggered = strftime("%s","now") '
|
||||||
|
'WHERE session_key = ? AND rating_key = ?',
|
||||||
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||||
|
|
||||||
|
logger.debug(u"PlexPy Monitor :: Stream buffering. Count is now %s. Last triggered %s."
|
||||||
|
% (buffer_values[0][0], buffer_values[0][1]))
|
||||||
|
|
||||||
|
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
||||||
|
# Don't trigger if state is buffer as some clients push the progress to the end when
|
||||||
|
# buffering on start.
|
||||||
|
if session['view_offset'] and session['duration'] and session['state'] != 'buffering':
|
||||||
|
if helpers.get_percent(session['view_offset'],
|
||||||
|
session['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
||||||
|
# Push any notifications -
|
||||||
|
# Push it on it's own thread so we don't hold up our db actions
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# The user has stopped playing a stream
|
||||||
|
logger.debug(u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
||||||
|
% (stream['session_key'], stream['rating_key']))
|
||||||
|
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
||||||
|
[stream['session_key'], stream['rating_key']])
|
||||||
|
|
||||||
|
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
||||||
|
if stream['view_offset'] and stream['duration']:
|
||||||
|
if helpers.get_percent(stream['view_offset'],
|
||||||
|
stream['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
||||||
|
# Push any notifications -
|
||||||
|
# Push it on it's own thread so we don't hold up our db actions
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
||||||
|
|
||||||
|
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=stream, notify_action='stop')).start()
|
||||||
|
|
||||||
|
# Write the item history on playback stop
|
||||||
|
monitor_process.write_session_history(session=stream)
|
||||||
|
|
||||||
|
# Process the newly received session data
|
||||||
|
for session in media_container:
|
||||||
|
monitor_process.write_session(session)
|
||||||
|
else:
|
||||||
|
logger.debug(u"PlexPy Monitor :: Unable to read session list.")
|
||||||
|
|
||||||
|
int_ping_count += 1
|
||||||
|
logger.warn(u"PlexPy Monitor :: Unable to get an internal response from the server, ping attempt %s." \
|
||||||
|
% str(int_ping_count))
|
||||||
|
|
||||||
|
if int_ping_count == 3:
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify_timeline,
|
||||||
|
kwargs=dict(notify_action='intdown')).start()
|
||||||
|
|
||||||
|
|
||||||
|
def check_recently_added():
|
||||||
|
|
||||||
|
with monitor_lock:
|
||||||
|
# add delay to allow for metadata processing
|
||||||
|
delay = plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY
|
||||||
|
time_threshold = int(time.time()) - delay
|
||||||
|
time_interval = plexpy.CONFIG.MONITORING_INTERVAL
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
recently_added_list = pms_connect.get_recently_added_details(count='10')
|
||||||
|
|
||||||
|
if recently_added_list:
|
||||||
|
recently_added = recently_added_list['recently_added']
|
||||||
|
|
||||||
|
for item in recently_added:
|
||||||
|
metadata = []
|
||||||
|
|
||||||
|
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||||
|
if item['media_type'] == 'movie':
|
||||||
|
metadata_list = pms_connect.get_metadata_details(item['rating_key'])
|
||||||
|
if metadata_list:
|
||||||
|
metadata = [metadata_list['metadata']]
|
||||||
|
else:
|
||||||
|
logger.error(u"PlexPy Monitor :: Unable to retrieve metadata for rating_key %s" \
|
||||||
|
% str(item['rating_key']))
|
||||||
|
|
||||||
|
else:
|
||||||
|
metadata_list = pms_connect.get_metadata_children_details(item['rating_key'])
|
||||||
|
if metadata_list:
|
||||||
|
metadata = metadata_list['metadata']
|
||||||
|
else:
|
||||||
|
logger.error(u"PlexPy Monitor :: Unable to retrieve children metadata for rating_key %s" \
|
||||||
|
% str(item['rating_key']))
|
||||||
|
|
||||||
|
if metadata:
|
||||||
|
if not plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
|
||||||
|
for item in metadata:
|
||||||
|
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||||
|
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify_timeline,
|
||||||
|
kwargs=dict(timeline_data=item, notify_action='created')).start()
|
||||||
|
|
||||||
|
else:
|
||||||
|
item = max(metadata, key=lambda x:x['added_at'])
|
||||||
|
|
||||||
|
if 0 < time_threshold - int(item['added_at']) <= time_interval:
|
||||||
|
if item['media_type'] == 'episode' or item['media_type'] == 'track':
|
||||||
|
metadata_list = pms_connect.get_metadata_details(item['grandparent_rating_key'])
|
||||||
|
|
||||||
|
if metadata_list:
|
||||||
|
item = metadata_list['metadata']
|
||||||
|
else:
|
||||||
|
logger.error(u"PlexPy Monitor :: Unable to retrieve grandparent metadata for grandparent_rating_key %s" \
|
||||||
|
% str(item['rating_key']))
|
||||||
|
|
||||||
|
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify_timeline,
|
||||||
|
kwargs=dict(timeline_data=item, notify_action='created')).start()
|
||||||
|
|
||||||
|
def check_server_response():
|
||||||
|
|
||||||
|
with monitor_lock:
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
server_response = pms_connect.get_server_response()
|
||||||
|
|
||||||
|
global ext_ping_count
|
||||||
|
|
||||||
|
# Check for remote access
|
||||||
|
if server_response:
|
||||||
|
|
||||||
|
mapping_state = server_response['mapping_state']
|
||||||
|
mapping_error = server_response['mapping_error']
|
||||||
|
|
||||||
|
# Check if the port is mapped
|
||||||
|
if not mapping_state == 'mapped':
|
||||||
|
ext_ping_count += 1
|
||||||
|
logger.warn(u"PlexPy Monitor :: Plex remote access port not mapped, ping attempt %s." \
|
||||||
|
% str(ext_ping_count))
|
||||||
|
# Check if the port is open
|
||||||
|
elif mapping_error == 'unreachable':
|
||||||
|
ext_ping_count += 1
|
||||||
|
logger.warn(u"PlexPy Monitor :: Plex remote access port mapped, but mapping failed, ping attempt %s." \
|
||||||
|
% str(ext_ping_count))
|
||||||
|
# Reset external ping counter
|
||||||
|
else:
|
||||||
|
ext_ping_count = 0
|
||||||
|
|
||||||
|
if ext_ping_count == 3:
|
||||||
|
# Fire off notifications
|
||||||
|
threading.Thread(target=notification_handler.notify_timeline,
|
||||||
|
kwargs=dict(notify_action='extdown')).start()
|
||||||
422
plexpy/activity_processor.py
Normal file
422
plexpy/activity_processor.py
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PlexPy is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from plexpy import logger, pmsconnect, notification_handler, log_reader, database
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import plexpy
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityProcessor(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
def write_session(self, session=None, notify=True):
|
||||||
|
if session:
|
||||||
|
values = {'session_key': session['session_key'],
|
||||||
|
'rating_key': session['rating_key'],
|
||||||
|
'media_type': session['media_type'],
|
||||||
|
'state': session['state'],
|
||||||
|
'user_id': session['user_id'],
|
||||||
|
'user': session['user'],
|
||||||
|
'machine_id': session['machine_id'],
|
||||||
|
'title': session['title'],
|
||||||
|
'parent_title': session['parent_title'],
|
||||||
|
'grandparent_title': session['grandparent_title'],
|
||||||
|
'friendly_name': session['friendly_name'],
|
||||||
|
#'ip_address': session['ip_address'],
|
||||||
|
'player': session['player'],
|
||||||
|
'platform': session['platform'],
|
||||||
|
'parent_rating_key': session['parent_rating_key'],
|
||||||
|
'grandparent_rating_key': session['grandparent_rating_key'],
|
||||||
|
'view_offset': session['view_offset'],
|
||||||
|
'duration': session['duration'],
|
||||||
|
'video_decision': session['video_decision'],
|
||||||
|
'audio_decision': session['audio_decision'],
|
||||||
|
'width': session['width'],
|
||||||
|
'height': session['height'],
|
||||||
|
'container': session['container'],
|
||||||
|
'video_codec': session['video_codec'],
|
||||||
|
'audio_codec': session['audio_codec'],
|
||||||
|
'bitrate': session['bitrate'],
|
||||||
|
'video_resolution': session['video_resolution'],
|
||||||
|
'video_framerate': session['video_framerate'],
|
||||||
|
'aspect_ratio': session['aspect_ratio'],
|
||||||
|
'audio_channels': session['audio_channels'],
|
||||||
|
'transcode_protocol': session['transcode_protocol'],
|
||||||
|
'transcode_container': session['transcode_container'],
|
||||||
|
'transcode_video_codec': session['transcode_video_codec'],
|
||||||
|
'transcode_audio_codec': session['transcode_audio_codec'],
|
||||||
|
'transcode_audio_channels': session['transcode_audio_channels'],
|
||||||
|
'transcode_width': session['transcode_width'],
|
||||||
|
'transcode_height': session['transcode_height']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add ip_address back into values
|
||||||
|
if session['ip_address']:
|
||||||
|
values.update({'ip_address': session['ip_address']})
|
||||||
|
|
||||||
|
keys = {'session_key': session['session_key'],
|
||||||
|
'rating_key': session['rating_key']}
|
||||||
|
|
||||||
|
result = self.db.upsert('sessions', values, keys)
|
||||||
|
|
||||||
|
if result == 'insert':
|
||||||
|
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
||||||
|
if notify:
|
||||||
|
values.update({'ip_address': session['ip_address']})
|
||||||
|
threading.Thread(target=notification_handler.notify,
|
||||||
|
kwargs=dict(stream_data=values, notify_action='play')).start()
|
||||||
|
|
||||||
|
# If it's our first write then time stamp it.
|
||||||
|
started = int(time.time())
|
||||||
|
timestamp = {'started': started}
|
||||||
|
self.db.upsert('sessions', timestamp, keys)
|
||||||
|
|
||||||
|
# Try and grab IP address from logs (fallback if not on PMS 0.9.14 and above)
|
||||||
|
if not session['ip_address']:
|
||||||
|
if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER:
|
||||||
|
ip_address = self.find_session_ip(rating_key=session['rating_key'],
|
||||||
|
machine_id=session['machine_id'])
|
||||||
|
ip_address = {'ip_address': ip_address}
|
||||||
|
self.db.upsert('sessions', ip_address, keys)
|
||||||
|
|
||||||
|
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
|
||||||
|
from plexpy import users
|
||||||
|
|
||||||
|
user_data = users.Users()
|
||||||
|
user_details = user_data.get_user_friendly_name(user=session['user'])
|
||||||
|
|
||||||
|
if session:
|
||||||
|
logging_enabled = False
|
||||||
|
|
||||||
|
if is_import:
|
||||||
|
if str(session['stopped']).isdigit():
|
||||||
|
stopped = int(session['stopped'])
|
||||||
|
else:
|
||||||
|
stopped = int(time.time())
|
||||||
|
else:
|
||||||
|
stopped = int(time.time())
|
||||||
|
|
||||||
|
if plexpy.CONFIG.MOVIE_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
|
||||||
|
session['media_type'] == 'movie':
|
||||||
|
logging_enabled = True
|
||||||
|
elif plexpy.CONFIG.TV_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
|
||||||
|
session['media_type'] == 'episode':
|
||||||
|
logging_enabled = True
|
||||||
|
elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
|
||||||
|
session['media_type'] == 'track':
|
||||||
|
logging_enabled = True
|
||||||
|
else:
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: ratingKey %s not logged. Does not meet logging criteria. "
|
||||||
|
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
||||||
|
|
||||||
|
if str(session['paused_counter']).isdigit():
|
||||||
|
real_play_time = stopped - session['started'] - int(session['paused_counter'])
|
||||||
|
else:
|
||||||
|
real_play_time = stopped - session['started']
|
||||||
|
|
||||||
|
if plexpy.CONFIG.LOGGING_IGNORE_INTERVAL and not is_import:
|
||||||
|
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||||
|
(real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
||||||
|
logging_enabled = False
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||||
|
u"seconds, so we're not logging it." %
|
||||||
|
(session['rating_key'], str(real_play_time), plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
||||||
|
if session['media_type'] == 'track' and not is_import:
|
||||||
|
if real_play_time < 15 and session['duration'] >= 30:
|
||||||
|
logging_enabled = False
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Play duration for ratingKey %s is %s secs, "
|
||||||
|
u"looks like it was skipped so we're not logging it" %
|
||||||
|
(session['rating_key'], str(real_play_time)))
|
||||||
|
elif is_import and import_ignore_interval:
|
||||||
|
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
||||||
|
(real_play_time < int(import_ignore_interval)):
|
||||||
|
logging_enabled = False
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
||||||
|
u"seconds, so we're not logging it." %
|
||||||
|
(session['rating_key'], str(real_play_time),
|
||||||
|
import_ignore_interval))
|
||||||
|
|
||||||
|
if not user_details['keep_history'] and not is_import:
|
||||||
|
logging_enabled = False
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: History logging for user '%s' is disabled." % session['user'])
|
||||||
|
|
||||||
|
if logging_enabled:
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Attempting to write to session_history table...")
|
||||||
|
query = 'INSERT INTO session_history (started, stopped, rating_key, parent_rating_key, ' \
|
||||||
|
'grandparent_rating_key, media_type, user_id, user, ip_address, paused_counter, player, ' \
|
||||||
|
'platform, machine_id, view_offset) VALUES ' \
|
||||||
|
'(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
|
||||||
|
args = [session['started'], stopped, session['rating_key'], session['parent_rating_key'],
|
||||||
|
session['grandparent_rating_key'], session['media_type'], session['user_id'], session['user'],
|
||||||
|
session['ip_address'], session['paused_counter'], session['player'], session['platform'],
|
||||||
|
session['machine_id'], session['view_offset']]
|
||||||
|
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Writing session_history transaction...")
|
||||||
|
self.db.action(query=query, args=args)
|
||||||
|
|
||||||
|
# Check if we should group the session, select the last two rows from the user
|
||||||
|
query = 'SELECT id, rating_key, user_id, reference_id FROM session_history \
|
||||||
|
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
|
||||||
|
|
||||||
|
args = [session['user_id']]
|
||||||
|
|
||||||
|
result = self.db.select(query=query, args=args)
|
||||||
|
|
||||||
|
new_session = {'id': result[0][0],
|
||||||
|
'rating_key': result[0][1],
|
||||||
|
'user_id': result[0][2],
|
||||||
|
'reference_id': result[0][3]}
|
||||||
|
|
||||||
|
if len(result) == 1:
|
||||||
|
prev_session = None
|
||||||
|
else:
|
||||||
|
prev_session = {'id': result[1][0],
|
||||||
|
'rating_key': result[1][1],
|
||||||
|
'user_id': result[1][2],
|
||||||
|
'reference_id': result[1][3]}
|
||||||
|
|
||||||
|
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||||
|
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
||||||
|
if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key']):
|
||||||
|
args = [prev_session['reference_id'], new_session['id']]
|
||||||
|
else:
|
||||||
|
args = [new_session['id'], new_session['id']]
|
||||||
|
|
||||||
|
self.db.action(query=query, args=args)
|
||||||
|
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Successfully written history item, last id for session_history is %s"
|
||||||
|
# % last_id)
|
||||||
|
|
||||||
|
# Write the session_history_media_info table
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Attempting to write to session_history_media_info table...")
|
||||||
|
query = 'INSERT INTO session_history_media_info (id, rating_key, video_decision, audio_decision, ' \
|
||||||
|
'duration, width, height, container, video_codec, audio_codec, bitrate, video_resolution, ' \
|
||||||
|
'video_framerate, aspect_ratio, audio_channels, transcode_protocol, transcode_container, ' \
|
||||||
|
'transcode_video_codec, transcode_audio_codec, transcode_audio_channels, transcode_width, ' \
|
||||||
|
'transcode_height) VALUES ' \
|
||||||
|
'(last_insert_rowid(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
|
||||||
|
args = [session['rating_key'], session['video_decision'], session['audio_decision'],
|
||||||
|
session['duration'], session['width'], session['height'], session['container'],
|
||||||
|
session['video_codec'], session['audio_codec'], session['bitrate'],
|
||||||
|
session['video_resolution'], session['video_framerate'], session['aspect_ratio'],
|
||||||
|
session['audio_channels'], session['transcode_protocol'], session['transcode_container'],
|
||||||
|
session['transcode_video_codec'], session['transcode_audio_codec'],
|
||||||
|
session['transcode_audio_channels'], session['transcode_width'], session['transcode_height']]
|
||||||
|
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Writing session_history_media_info transaction...")
|
||||||
|
self.db.action(query=query, args=args)
|
||||||
|
|
||||||
|
if not is_import:
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
||||||
|
metadata = result['metadata']
|
||||||
|
else:
|
||||||
|
metadata = import_metadata
|
||||||
|
|
||||||
|
# Write the session_history_metadata table
|
||||||
|
directors = ";".join(metadata['directors'])
|
||||||
|
writers = ";".join(metadata['writers'])
|
||||||
|
actors = ";".join(metadata['actors'])
|
||||||
|
genres = ";".join(metadata['genres'])
|
||||||
|
|
||||||
|
# Build media item title
|
||||||
|
if session['media_type'] == 'episode' or session['media_type'] == 'track':
|
||||||
|
full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||||
|
elif session['media_type'] == 'movie':
|
||||||
|
full_title = metadata['title']
|
||||||
|
else:
|
||||||
|
full_title = metadata['title']
|
||||||
|
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Attempting to write to session_history_metadata table...")
|
||||||
|
query = 'INSERT INTO session_history_metadata (id, rating_key, parent_rating_key, ' \
|
||||||
|
'grandparent_rating_key, title, parent_title, grandparent_title, full_title, media_index, ' \
|
||||||
|
'parent_media_index, thumb, parent_thumb, grandparent_thumb, art, media_type, year, ' \
|
||||||
|
'originally_available_at, added_at, updated_at, last_viewed_at, content_rating, summary, ' \
|
||||||
|
'tagline, rating, duration, guid, directors, writers, actors, genres, studio) VALUES ' \
|
||||||
|
'(last_insert_rowid(), ' \
|
||||||
|
'?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
|
||||||
|
args = [session['rating_key'], session['parent_rating_key'], session['grandparent_rating_key'],
|
||||||
|
session['title'], session['parent_title'], session['grandparent_title'], full_title,
|
||||||
|
metadata['index'], metadata['parent_index'], metadata['thumb'], metadata['parent_thumb'],
|
||||||
|
metadata['grandparent_thumb'], metadata['art'], session['media_type'], metadata['year'],
|
||||||
|
metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
||||||
|
metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['tagline'],
|
||||||
|
metadata['rating'], metadata['duration'], metadata['guid'], directors, writers, actors, genres, metadata['studio']]
|
||||||
|
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Writing session_history_metadata transaction...")
|
||||||
|
self.db.action(query=query, args=args)
|
||||||
|
|
||||||
|
def find_session_ip(self, rating_key=None, machine_id=None):
|
||||||
|
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Requesting log lines...")
|
||||||
|
log_lines = log_reader.get_log_tail(window=5000, parsed=False)
|
||||||
|
|
||||||
|
rating_key_line = 'ratingKey=' + rating_key
|
||||||
|
rating_key_line_2 = 'metadata%2F' + rating_key
|
||||||
|
machine_id_line = 'session=' + machine_id
|
||||||
|
|
||||||
|
for line in reversed(log_lines):
|
||||||
|
# We're good if we find a line with both machine id and rating key
|
||||||
|
# This is usually when there is a transcode session
|
||||||
|
if machine_id_line in line and (rating_key_line in line or rating_key_line_2 in line):
|
||||||
|
# Currently only checking for ipv4 addresses
|
||||||
|
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line)
|
||||||
|
if ipv4:
|
||||||
|
# The logged IP will always be the first match and we don't want localhost entries
|
||||||
|
if ipv4[0] != '127.0.0.1':
|
||||||
|
# check if IPv4 mapped IPv6 address (::ffff:xxx.xxx.xxx.xxx)
|
||||||
|
#if '::ffff:' + ipv4[0] in line:
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s "
|
||||||
|
# u"and machineIdentifier %s."
|
||||||
|
# % ('::ffff:' + ipv4[0], rating_key, machine_id))
|
||||||
|
# return '::ffff:' + ipv4[0]
|
||||||
|
#else:
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s "
|
||||||
|
u"and machineIdentifier %s."
|
||||||
|
% (ipv4[0], rating_key, machine_id))
|
||||||
|
return ipv4[0]
|
||||||
|
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on first pass. "
|
||||||
|
u"Attempting fallback check in 5 seconds...")
|
||||||
|
|
||||||
|
# Wait for the log to catch up and read in new lines
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Requesting log lines...")
|
||||||
|
log_lines = log_reader.get_log_tail(window=5000, parsed=False)
|
||||||
|
|
||||||
|
for line in reversed(log_lines):
|
||||||
|
if 'GET /:/timeline' in line and (rating_key_line in line or rating_key_line_2 in line):
|
||||||
|
# Currently only checking for ipv4 addresses
|
||||||
|
# This method can return the wrong IP address if more than one user
|
||||||
|
# starts watching the same media item around the same time.
|
||||||
|
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line)
|
||||||
|
if ipv4:
|
||||||
|
# The logged IP will always be the first match and we don't want localhost entries
|
||||||
|
if ipv4[0] != '127.0.0.1':
|
||||||
|
#if '::ffff:' + ipv4[0] in line:
|
||||||
|
# logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." %
|
||||||
|
# ('::ffff:' + ipv4[0], rating_key))
|
||||||
|
# return '::ffff:' + ipv4[0]
|
||||||
|
#else:
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." %
|
||||||
|
(ipv4[0], rating_key))
|
||||||
|
return ipv4[0]
|
||||||
|
|
||||||
|
logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on fallback search. Not logging IP address.")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_session_by_key(self, session_key=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
result = self.db.select('SELECT started, session_key, rating_key, media_type, title, parent_title, '
|
||||||
|
'grandparent_title, user_id, user, friendly_name, ip_address, player, '
|
||||||
|
'platform, machine_id, parent_rating_key, grandparent_rating_key, state, '
|
||||||
|
'view_offset, duration, video_decision, audio_decision, width, height, '
|
||||||
|
'container, video_codec, audio_codec, bitrate, video_resolution, '
|
||||||
|
'video_framerate, aspect_ratio, audio_channels, transcode_protocol, '
|
||||||
|
'transcode_container, transcode_video_codec, transcode_audio_codec, '
|
||||||
|
'transcode_audio_channels, transcode_width, transcode_height, '
|
||||||
|
'paused_counter, last_paused '
|
||||||
|
'FROM sessions WHERE session_key = ? LIMIT 1', args=[session_key])
|
||||||
|
for session in result:
|
||||||
|
if session:
|
||||||
|
return session
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_session_state(self, session_key=None, state=None, view_offset=0):
|
||||||
|
if str(session_key).isdigit() and str(view_offset).isdigit():
|
||||||
|
values = {'view_offset': int(view_offset)}
|
||||||
|
if state:
|
||||||
|
values['state'] = state
|
||||||
|
|
||||||
|
keys = {'session_key': session_key}
|
||||||
|
result = self.db.upsert('sessions', values, keys)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_session(self, session_key=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
self.db.action('DELETE FROM sessions WHERE session_key = ?', [session_key])
|
||||||
|
|
||||||
|
def set_session_last_paused(self, session_key=None, timestamp=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
result = self.db.select('SELECT last_paused, paused_counter '
|
||||||
|
'FROM sessions '
|
||||||
|
'WHERE session_key = ?', args=[session_key])
|
||||||
|
|
||||||
|
paused_counter = None
|
||||||
|
for session in result:
|
||||||
|
if session['last_paused']:
|
||||||
|
paused_offset = int(time.time()) - int(session['last_paused'])
|
||||||
|
if session['paused_counter']:
|
||||||
|
paused_counter = int(session['paused_counter']) + int(paused_offset)
|
||||||
|
else:
|
||||||
|
paused_counter = int(paused_offset)
|
||||||
|
|
||||||
|
values = {'state': 'playing',
|
||||||
|
'last_paused': timestamp
|
||||||
|
}
|
||||||
|
if paused_counter:
|
||||||
|
values['paused_counter'] = paused_counter
|
||||||
|
|
||||||
|
keys = {'session_key': session_key}
|
||||||
|
self.db.upsert('sessions', values, keys)
|
||||||
|
|
||||||
|
def increment_session_buffer_count(self, session_key=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
self.db.action('UPDATE sessions SET buffer_count = buffer_count + 1 '
|
||||||
|
'WHERE session_key = ?',
|
||||||
|
[session_key])
|
||||||
|
|
||||||
|
def get_session_buffer_count(self, session_key=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
buffer_count = self.db.select_single('SELECT buffer_count '
|
||||||
|
'FROM sessions '
|
||||||
|
'WHERE session_key = ?',
|
||||||
|
[session_key])
|
||||||
|
if buffer_count:
|
||||||
|
return buffer_count
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def set_session_buffer_trigger_time(self, session_key=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
self.db.action('UPDATE sessions SET buffer_last_triggered = strftime("%s","now") '
|
||||||
|
'WHERE session_key = ?',
|
||||||
|
[session_key])
|
||||||
|
|
||||||
|
def get_session_buffer_trigger_time(self, session_key=None):
|
||||||
|
if str(session_key).isdigit():
|
||||||
|
last_time = self.db.select_single('SELECT buffer_last_triggered '
|
||||||
|
'FROM sessions '
|
||||||
|
'WHERE session_key = ?',
|
||||||
|
[session_key])
|
||||||
|
if last_time:
|
||||||
|
return last_time
|
||||||
|
|
||||||
|
return None
|
||||||
547
plexpy/api.py
547
plexpy/api.py
@@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
@@ -13,86 +16,143 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import db, cache, versioncheck, logger, helpers
|
from plexpy import versioncheck, logger, plextv, pmsconnect, datafactory, graphs, users
|
||||||
|
import os
|
||||||
import plexpy
|
import plexpy
|
||||||
import json
|
import json
|
||||||
from xml.dom import minidom
|
import traceback
|
||||||
|
import cherrypy
|
||||||
|
import re
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
cmd_list = ['getHistory', 'getLogs', 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update']
|
cmd_list = ['getLogs', 'getVersion', 'checkGithub', 'shutdown',
|
||||||
|
'getSettings', 'restart', 'update', 'getApikey', 'getHistory',
|
||||||
|
'getMetadata', 'getUserips', 'getPlayby', 'getSync']
|
||||||
|
|
||||||
|
|
||||||
class Api(object):
|
class Api(object):
|
||||||
|
def __init__(self, out='json'):
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
self.apikey = None
|
self.apikey = None
|
||||||
|
self.authenticated = False
|
||||||
self.cmd = None
|
self.cmd = None
|
||||||
self.id = None
|
|
||||||
|
|
||||||
self.kwargs = None
|
self.kwargs = None
|
||||||
|
# For the responses
|
||||||
self.data = None
|
self.data = None
|
||||||
|
self.msg = None
|
||||||
|
self.result_type = 'error'
|
||||||
|
# Possible general params
|
||||||
self.callback = None
|
self.callback = None
|
||||||
|
self.out_type = out
|
||||||
|
self.debug = None
|
||||||
|
|
||||||
def checkParams(self, *args, **kwargs):
|
def checkParams(self, *args, **kwargs):
|
||||||
|
|
||||||
if not plexpy.CONFIG.API_ENABLED:
|
if not plexpy.CONFIG.API_ENABLED:
|
||||||
self.data = 'API not enabled'
|
self.msg = 'API not enabled'
|
||||||
return
|
elif not plexpy.CONFIG.API_KEY:
|
||||||
if not plexpy.CONFIG.API_KEY:
|
self.msg = 'API key not generated'
|
||||||
self.data = 'API key not generated'
|
elif len(plexpy.CONFIG.API_KEY) != 32:
|
||||||
return
|
self.msg = 'API key not generated correctly'
|
||||||
if len(plexpy.CONFIG.API_KEY) != 32:
|
elif 'apikey' not in kwargs:
|
||||||
self.data = 'API key not generated correctly'
|
self.msg = 'Parameter apikey is required'
|
||||||
return
|
elif kwargs.get('apikey', '') != plexpy.CONFIG.API_KEY:
|
||||||
|
self.msg = 'Invalid apikey'
|
||||||
|
elif 'cmd' not in kwargs:
|
||||||
|
self.msg = 'Parameter %s required. possible commands are: %s' % ', '.join(cmd_list)
|
||||||
|
elif 'cmd' in kwargs and kwargs.get('cmd') not in cmd_list:
|
||||||
|
self.msg = 'Unknown command, %s possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(cmd_list))
|
||||||
|
|
||||||
if 'apikey' not in kwargs:
|
# Set default values or remove them from kwargs
|
||||||
self.data = 'Missing api key'
|
|
||||||
return
|
|
||||||
|
|
||||||
if kwargs['apikey'] != plexpy.CONFIG.API_KEY:
|
self.callback = kwargs.pop('callback', None)
|
||||||
self.data = 'Incorrect API key'
|
self.apikey = kwargs.pop('apikey', None)
|
||||||
return
|
self.cmd = kwargs.pop('cmd', None)
|
||||||
else:
|
self.debug = kwargs.pop('debug', False)
|
||||||
self.apikey = kwargs.pop('apikey')
|
# Allow override for the api.
|
||||||
|
self.out_type = kwargs.pop('out_type', 'json')
|
||||||
|
|
||||||
if 'cmd' not in kwargs:
|
if self.apikey == plexpy.CONFIG.API_KEY and plexpy.CONFIG.API_ENABLED and self.cmd in cmd_list:
|
||||||
self.data = 'Missing parameter: cmd'
|
self.authenticated = True
|
||||||
return
|
self.msg = None
|
||||||
|
elif self.cmd == 'getApikey' and plexpy.CONFIG.API_ENABLED:
|
||||||
if kwargs['cmd'] not in cmd_list:
|
self.authenticated = True
|
||||||
self.data = 'Unknown command: %s' % kwargs['cmd']
|
# Remove the old error msg
|
||||||
return
|
self.msg = None
|
||||||
else:
|
|
||||||
self.cmd = kwargs.pop('cmd')
|
|
||||||
|
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.data = 'OK'
|
|
||||||
|
def _responds(self, result_type='success', data=None, msg=''):
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
|
return {"response": {"result": result_type, "message": msg, "data": data}}
|
||||||
|
|
||||||
|
def _out_as(self, out):
|
||||||
|
|
||||||
|
if self.out_type == 'json':
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||||
|
try:
|
||||||
|
out = json.dumps(out, indent=4, sort_keys=True)
|
||||||
|
if self.callback is not None:
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/javascript'
|
||||||
|
# wrap with JSONP call if requested
|
||||||
|
out = self.callback + '(' + out + ');'
|
||||||
|
# if we fail to generate the output fake an error
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(u"API :: " + traceback.format_exc())
|
||||||
|
out['message'] = traceback.format_exc()
|
||||||
|
out['result'] = 'error'
|
||||||
|
if self.out_type == 'xml':
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/xml'
|
||||||
|
try:
|
||||||
|
out = xmltodict.unparse(out, pretty=True)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error('Failed to parse xml result')
|
||||||
|
try:
|
||||||
|
out['message'] = e
|
||||||
|
out['result'] = 'error'
|
||||||
|
out = xmltodict.unparse(out, pretty=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Failed to parse xml result error message')
|
||||||
|
out = '''<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response>
|
||||||
|
<message>%s</message>
|
||||||
|
<data></data>
|
||||||
|
<result>error</result>
|
||||||
|
</response>
|
||||||
|
''' % e
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
def fetchData(self):
|
def fetchData(self):
|
||||||
|
|
||||||
if self.data == 'OK':
|
logger.info('Recieved API command: %s' % self.cmd)
|
||||||
logger.info('Recieved API command: %s', self.cmd)
|
if self.cmd and self.authenticated:
|
||||||
methodToCall = getattr(self, "_" + self.cmd)
|
methodtocall = getattr(self, "_" + self.cmd)
|
||||||
methodToCall(**self.kwargs)
|
# Let the traceback hit cherrypy so we can
|
||||||
if 'callback' not in self.kwargs:
|
# see the traceback there
|
||||||
if isinstance(self.data, basestring):
|
if self.debug:
|
||||||
return self.data
|
methodtocall(**self.kwargs)
|
||||||
else:
|
else:
|
||||||
return json.dumps(self.data)
|
try:
|
||||||
else:
|
methodtocall(**self.kwargs)
|
||||||
self.callback = self.kwargs['callback']
|
except Exception as e:
|
||||||
self.data = json.dumps(self.data)
|
logger.error(traceback.format_exc())
|
||||||
self.data = self.callback + '(' + self.data + ');'
|
|
||||||
return self.data
|
# Im just lazy, fix me plx
|
||||||
else:
|
if self.data or isinstance(self.data, (dict, list)):
|
||||||
return self.data
|
if len(self.data):
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
|
return self._out_as(self._responds(result_type=self.result_type, msg=self.msg, data=self.data))
|
||||||
|
|
||||||
def _dic_from_query(self, query):
|
def _dic_from_query(self, query):
|
||||||
|
|
||||||
myDB = db.DBConnection()
|
myDB = database.DBConnection()
|
||||||
rows = myDB.select(query)
|
rows = myDB.select(query)
|
||||||
|
|
||||||
rows_as_dic = []
|
rows_as_dic = []
|
||||||
@@ -103,104 +163,115 @@ class Api(object):
|
|||||||
|
|
||||||
return rows_as_dic
|
return rows_as_dic
|
||||||
|
|
||||||
def _getHistory(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', sSortDir_0='asc', **kwargs):
|
def _getApikey(self, username='', password=''):
|
||||||
iDisplayStart = int(iDisplayStart)
|
""" Returns api key, requires username and password is active """
|
||||||
iDisplayLength = int(iDisplayLength)
|
|
||||||
filtered = []
|
|
||||||
totalcount = 0
|
|
||||||
myDB = db.DBConnection()
|
|
||||||
db_table = db.DBConnection().get_history_table_name()
|
|
||||||
|
|
||||||
sortcolumn = 'time'
|
apikey = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32]
|
||||||
sortbyhavepercent = False
|
if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
if iSortCol_0 == '1':
|
if username == plexpy.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||||
sortcolumn = 'user'
|
if plexpy.CONFIG.API_KEY:
|
||||||
if iSortCol_0 == '2':
|
self.data = plexpy.CONFIG.API_KEY
|
||||||
sortcolumn = 'platform'
|
|
||||||
elif iSortCol_0 == '3':
|
|
||||||
sortcolumn = 'ip_address'
|
|
||||||
elif iSortCol_0 == '4':
|
|
||||||
sortcolumn = 'title'
|
|
||||||
elif iSortCol_0 == '5':
|
|
||||||
sortcolumn = 'time'
|
|
||||||
elif iSortCol_0 == '6':
|
|
||||||
sortcolumn = 'paused_counter'
|
|
||||||
elif iSortCol_0 == '7':
|
|
||||||
sortcolumn = 'stopped'
|
|
||||||
elif iSortCol_0 == '8':
|
|
||||||
sortbyhavepercent = True
|
|
||||||
|
|
||||||
if sSearch == "":
|
|
||||||
query = 'SELECT * from %s order by %s COLLATE NOCASE %s' % (db_table, sortcolumn, sSortDir_0)
|
|
||||||
filtered = myDB.select(query)
|
|
||||||
totalcount = len(filtered)
|
|
||||||
else:
|
else:
|
||||||
query = 'SELECT * from ' + db_table + ' WHERE user LIKE "%' + sSearch + \
|
self.data = apikey
|
||||||
'%" OR title LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (sortcolumn, sSortDir_0)
|
plexpy.CONFIG.API_KEY = apikey
|
||||||
filtered = myDB.select(query)
|
plexpy.CONFIG.write()
|
||||||
totalcount = myDB.select('SELECT COUNT(*) from processed')[0][0]
|
else:
|
||||||
|
self.msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
||||||
|
else:
|
||||||
|
if plexpy.CONFIG.API_KEY:
|
||||||
|
self.data = plexpy.CONFIG.API_KEY
|
||||||
|
else:
|
||||||
|
# Make a apikey if the doesn't exist
|
||||||
|
self.data = apikey
|
||||||
|
plexpy.CONFIG.API_KEY = apikey
|
||||||
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
history = filtered[iDisplayStart:(iDisplayStart + iDisplayLength)]
|
return self.data
|
||||||
rows = []
|
|
||||||
for item in history:
|
def _getLogs(self, sort='', search='', order='desc', regex='', **kwargs):
|
||||||
row = {"date": item['time'],
|
"""
|
||||||
"user": item["user"],
|
Returns the log
|
||||||
"platform": item["platform"],
|
|
||||||
"ip_address": item["ip_address"],
|
Returns [{"response":
|
||||||
"title": item["title"],
|
{"msg": "Hey",
|
||||||
"started": item["time"],
|
"result": "success"},
|
||||||
"paused": item["paused_counter"],
|
"data": [{"time": "29-sept.2015",
|
||||||
"stopped": item["stopped"],
|
"thread: "MainThread",
|
||||||
"duration": "",
|
"msg: "Called x from y",
|
||||||
"percent_complete": 0,
|
"loglevel": "DEBUG"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
if item['paused_counter'] > 0:
|
}
|
||||||
row['paused'] = item['paused_counter']
|
]
|
||||||
else:
|
"""
|
||||||
row['paused'] = 0
|
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, 'plexpy.log')
|
||||||
|
templog = []
|
||||||
|
start = int(kwargs.get('start', 0))
|
||||||
|
end = int(kwargs.get('end', 0))
|
||||||
|
|
||||||
if item['time']:
|
if regex:
|
||||||
if item['stopped'] > 0:
|
logger.debug('Filtering log using regex %s' % regex)
|
||||||
stopped = item['stopped']
|
reg = re.compile('u' + regex, flags=re.I)
|
||||||
else:
|
|
||||||
stopped = 0
|
|
||||||
if item['paused_counter'] > 0:
|
|
||||||
paused_counter = item['paused_counter']
|
|
||||||
else:
|
|
||||||
paused_counter = 0
|
|
||||||
|
|
||||||
row['duration'] = stopped - item['time'] + paused_counter
|
for line in open(logfile, 'r').readlines():
|
||||||
|
temp_loglevel_and_time = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml_parse = minidom.parseString(helpers.latinToAscii(item['xml']))
|
temp_loglevel_and_time = line.split('- ')
|
||||||
except IOError, e:
|
loglvl = temp_loglevel_and_time[1].split(' :')[0].strip()
|
||||||
logger.warn("Error parsing XML in PlexWatch db: %s" % e)
|
tl_tread = line.split(' :: ')
|
||||||
|
if loglvl is None:
|
||||||
xml_head = xml_parse.getElementsByTagName('opt')
|
msg = line.replace('\n', '')
|
||||||
if not xml_head:
|
|
||||||
logger.warn("Error parsing XML in PlexWatch db: %s" % e)
|
|
||||||
|
|
||||||
for s in xml_head:
|
|
||||||
if s.getAttribute('duration') and s.getAttribute('viewOffset'):
|
|
||||||
view_offset = helpers.cast_to_float(s.getAttribute('viewOffset'))
|
|
||||||
duration = helpers.cast_to_float(s.getAttribute('duration'))
|
|
||||||
if duration > 0:
|
|
||||||
row['percent_complete'] = (view_offset / duration)*100
|
|
||||||
else:
|
else:
|
||||||
row['percent_complete'] = 0
|
msg = line.split(' : ')[1].replace('\n', '')
|
||||||
|
thread = tl_tread[1].split(' : ')[0]
|
||||||
|
except IndexError:
|
||||||
|
# We assume this is a traceback
|
||||||
|
tl = (len(templog) - 1)
|
||||||
|
templog[tl]['msg'] += line.replace('\n', '')
|
||||||
|
continue
|
||||||
|
|
||||||
rows.append(row)
|
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
||||||
|
|
||||||
dict = {'iTotalDisplayRecords': len(filtered),
|
d = {
|
||||||
'iTotalRecords': totalcount,
|
'time': temp_loglevel_and_time[0],
|
||||||
'aaData': rows,
|
'loglevel': loglvl,
|
||||||
|
'msg': msg.replace('\n', ''),
|
||||||
|
'thread': thread
|
||||||
}
|
}
|
||||||
self.data = json.dumps(dict)
|
templog.append(d)
|
||||||
#cherrypy.response.headers['Content-type'] = 'application/json'
|
|
||||||
|
|
||||||
def _getLogs(self, **kwargs):
|
if end > 0:
|
||||||
pass
|
logger.debug('Slicing the log from %s to %s' % (start, end))
|
||||||
|
templog = templog[start:end]
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
logger.debug('Sorting log based on %s' % sort)
|
||||||
|
templog = sorted(templog, key=lambda k: k[sort])
|
||||||
|
|
||||||
|
if search:
|
||||||
|
logger.debug('Searching log values for %s' % search)
|
||||||
|
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
|
||||||
|
|
||||||
|
if len(tt):
|
||||||
|
templog = tt
|
||||||
|
|
||||||
|
if regex:
|
||||||
|
tt = []
|
||||||
|
for l in templog:
|
||||||
|
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
|
||||||
|
if reg.search(stringdict):
|
||||||
|
tt.append(l)
|
||||||
|
|
||||||
|
if len(tt):
|
||||||
|
templog = tt
|
||||||
|
|
||||||
|
if order == 'desc':
|
||||||
|
templog = templog[::-1]
|
||||||
|
|
||||||
|
self.data = templog
|
||||||
|
return templog
|
||||||
|
|
||||||
def _getVersion(self, **kwargs):
|
def _getVersion(self, **kwargs):
|
||||||
self.data = {
|
self.data = {
|
||||||
@@ -210,6 +281,7 @@ class Api(object):
|
|||||||
'latest_version': plexpy.LATEST_VERSION,
|
'latest_version': plexpy.LATEST_VERSION,
|
||||||
'commits_behind': plexpy.COMMITS_BEHIND,
|
'commits_behind': plexpy.COMMITS_BEHIND,
|
||||||
}
|
}
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
def _checkGithub(self, **kwargs):
|
def _checkGithub(self, **kwargs):
|
||||||
versioncheck.checkGithub()
|
versioncheck.checkGithub()
|
||||||
@@ -217,9 +289,212 @@ class Api(object):
|
|||||||
|
|
||||||
def _shutdown(self, **kwargs):
|
def _shutdown(self, **kwargs):
|
||||||
plexpy.SIGNAL = 'shutdown'
|
plexpy.SIGNAL = 'shutdown'
|
||||||
|
self.msg = 'Shutting down plexpy'
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
def _restart(self, **kwargs):
|
def _restart(self, **kwargs):
|
||||||
plexpy.SIGNAL = 'restart'
|
plexpy.SIGNAL = 'restart'
|
||||||
|
self.msg = 'Restarting plexpy'
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
def _update(self, **kwargs):
|
def _update(self, **kwargs):
|
||||||
plexpy.SIGNAL = 'update'
|
plexpy.SIGNAL = 'update'
|
||||||
|
self.msg = 'Updating plexpy'
|
||||||
|
self.result_type = 'success'
|
||||||
|
|
||||||
|
def _getHistory(self, user=None, user_id=None, rating_key='', parent_rating_key='', grandparent_rating_key='', start_date='', **kwargs):
|
||||||
|
|
||||||
|
custom_where = []
|
||||||
|
if user_id:
|
||||||
|
custom_where = [['user_id', user_id]]
|
||||||
|
elif user:
|
||||||
|
custom_where = [['user', user]]
|
||||||
|
if 'rating_key' in kwargs:
|
||||||
|
rating_key = kwargs.get('rating_key', "")
|
||||||
|
custom_where = [['rating_key', rating_key]]
|
||||||
|
if 'parent_rating_key' in kwargs:
|
||||||
|
rating_key = kwargs.get('parent_rating_key', "")
|
||||||
|
custom_where = [['parent_rating_key', rating_key]]
|
||||||
|
if 'grandparent_rating_key' in kwargs:
|
||||||
|
rating_key = kwargs.get('grandparent_rating_key', "")
|
||||||
|
custom_where = [['grandparent_rating_key', rating_key]]
|
||||||
|
if 'start_date' in kwargs:
|
||||||
|
start_date = kwargs.get('start_date', "")
|
||||||
|
custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]]
|
||||||
|
|
||||||
|
data_factory = datafactory.DataFactory()
|
||||||
|
history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where)
|
||||||
|
|
||||||
|
self.data = history
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
def _getSync(self, machine_id=None, user_id=None, **kwargs):
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
server_id = pms_connect.get_server_identity()
|
||||||
|
|
||||||
|
plex_tv = plextv.PlexTV()
|
||||||
|
if not machine_id:
|
||||||
|
result = plex_tv.get_synced_items(machine_id=server_id['machine_identifier'], user_id=user_id)
|
||||||
|
else:
|
||||||
|
result = plex_tv.get_synced_items(machine_id=machine_id, user_id=user_id)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.data = result
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
self.msg = 'Unable to retrieve sync data for user'
|
||||||
|
logger.warn('Unable to retrieve sync data for user.')
|
||||||
|
|
||||||
|
def _getMetadata(self, rating_key='', **kwargs):
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_metadata(rating_key, 'dict')
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.data = result
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
self.msg = 'Unable to retrive metadata %s' % rating_key
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
||||||
|
def _getSettings(self):
|
||||||
|
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
||||||
|
interface_list = [name for name in os.listdir(interface_dir) if
|
||||||
|
os.path.isdir(os.path.join(interface_dir, name))]
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"http_host": plexpy.CONFIG.HTTP_HOST,
|
||||||
|
"http_username": plexpy.CONFIG.HTTP_USERNAME,
|
||||||
|
"http_port": plexpy.CONFIG.HTTP_PORT,
|
||||||
|
"http_password": plexpy.CONFIG.HTTP_PASSWORD,
|
||||||
|
"launch_browser": bool(plexpy.CONFIG.LAUNCH_BROWSER),
|
||||||
|
"enable_https": bool(plexpy.CONFIG.ENABLE_HTTPS),
|
||||||
|
"https_cert": plexpy.CONFIG.HTTPS_CERT,
|
||||||
|
"https_key": plexpy.CONFIG.HTTPS_KEY,
|
||||||
|
"api_enabled": plexpy.CONFIG.API_ENABLED,
|
||||||
|
"api_key": plexpy.CONFIG.API_KEY,
|
||||||
|
"update_db_interval": plexpy.CONFIG.UPDATE_DB_INTERVAL,
|
||||||
|
"freeze_db": bool(plexpy.CONFIG.FREEZE_DB),
|
||||||
|
"log_dir": plexpy.CONFIG.LOG_DIR,
|
||||||
|
"cache_dir": plexpy.CONFIG.CACHE_DIR,
|
||||||
|
"check_github": bool(plexpy.CONFIG.CHECK_GITHUB),
|
||||||
|
"interface_list": interface_list,
|
||||||
|
"cache_sizemb": plexpy.CONFIG.CACHE_SIZEMB,
|
||||||
|
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
|
||||||
|
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||||
|
"pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER,
|
||||||
|
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||||
|
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||||
|
"pms_ssl": bool(plexpy.CONFIG.PMS_SSL),
|
||||||
|
"pms_use_bif": bool(plexpy.CONFIG.PMS_USE_BIF),
|
||||||
|
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||||
|
"date_format": plexpy.CONFIG.DATE_FORMAT,
|
||||||
|
"time_format": plexpy.CONFIG.TIME_FORMAT,
|
||||||
|
"grouping_global_history": bool(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
|
||||||
|
"grouping_user_history": bool(plexpy.CONFIG.GROUPING_USER_HISTORY),
|
||||||
|
"grouping_charts": bool(plexpy.CONFIG.GROUPING_CHARTS),
|
||||||
|
"movie_notify_enable": bool(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
|
||||||
|
"tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE),
|
||||||
|
"music_notify_enable": bool(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
|
||||||
|
"tv_notify_on_start": bool(plexpy.CONFIG.TV_NOTIFY_ON_START),
|
||||||
|
"movie_notify_on_start": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
|
||||||
|
"music_notify_on_start": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_START),
|
||||||
|
"tv_notify_on_stop": bool(plexpy.CONFIG.TV_NOTIFY_ON_STOP),
|
||||||
|
"movie_notify_on_stop": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_STOP),
|
||||||
|
"music_notify_on_stop": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_STOP),
|
||||||
|
"tv_notify_on_pause": bool(plexpy.CONFIG.TV_NOTIFY_ON_PAUSE),
|
||||||
|
"movie_notify_on_pause": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_PAUSE),
|
||||||
|
"music_notify_on_pause": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_PAUSE),
|
||||||
|
"monitoring_interval": plexpy.CONFIG.MONITORING_INTERVAL,
|
||||||
|
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
|
||||||
|
"refresh_users_on_startup": bool(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
|
||||||
|
"ip_logging_enable": bool(plexpy.CONFIG.IP_LOGGING_ENABLE),
|
||||||
|
"movie_logging_enable": bool(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
|
||||||
|
"tv_logging_enable": bool(plexpy.CONFIG.TV_LOGGING_ENABLE),
|
||||||
|
"music_logging_enable": bool(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
|
||||||
|
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
|
||||||
|
"pms_is_remote": bool(plexpy.CONFIG.PMS_IS_REMOTE),
|
||||||
|
"notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT,
|
||||||
|
"notify_on_start_subject_text": plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT,
|
||||||
|
"notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT,
|
||||||
|
"notify_on_stop_subject_text": plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT,
|
||||||
|
"notify_on_stop_body_text": plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT,
|
||||||
|
"notify_on_pause_subject_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT,
|
||||||
|
"notify_on_pause_body_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT,
|
||||||
|
"notify_on_resume_subject_text": plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT,
|
||||||
|
"notify_on_resume_body_text": plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT,
|
||||||
|
"notify_on_buffer_subject_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT,
|
||||||
|
"notify_on_buffer_body_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT,
|
||||||
|
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
|
||||||
|
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
|
||||||
|
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
|
||||||
|
"home_stats_type": bool(plexpy.CONFIG.HOME_STATS_TYPE),
|
||||||
|
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
|
||||||
|
"home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS,
|
||||||
|
"home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS,
|
||||||
|
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
|
||||||
|
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT
|
||||||
|
}
|
||||||
|
|
||||||
|
self.data = config
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _getUserips(self, user_id=None, user=None, **kwargs):
|
||||||
|
custom_where = []
|
||||||
|
if user_id:
|
||||||
|
custom_where = [['user_id', user_id]]
|
||||||
|
elif user:
|
||||||
|
custom_where = [['user', user]]
|
||||||
|
|
||||||
|
user_data = users.Users()
|
||||||
|
history = user_data.get_user_unique_ips(kwargs=kwargs,
|
||||||
|
custom_where=custom_where)
|
||||||
|
|
||||||
|
if history:
|
||||||
|
self.data = history
|
||||||
|
return history
|
||||||
|
else:
|
||||||
|
self.msg = 'Failed to find users ips'
|
||||||
|
|
||||||
|
def _getPlayby(self, time_range='30', y_axis='plays', playtype='total_plays_per_month', **kwargs):
|
||||||
|
|
||||||
|
graph = graphs.Graphs()
|
||||||
|
if playtype == 'total_plays_per_month':
|
||||||
|
result = graph.get_total_plays_per_month(y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_per_day':
|
||||||
|
result = graph.get_total_plays_per_day(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_per_hourofday':
|
||||||
|
result = graph.get_total_plays_per_hourofday(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_per_dayofweek':
|
||||||
|
result = graph.get_total_plays_per_dayofweek(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'stream_type_by_top_10_users':
|
||||||
|
result = graph.get_stream_type_by_top_10_users(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'stream_type_by_top_10_platforms':
|
||||||
|
result = graph.get_stream_type_by_top_10_platforms(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_by_stream_resolution':
|
||||||
|
result = graph.get_total_plays_by_stream_resolution(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_by_source_resolution':
|
||||||
|
result = graph.get_total_plays_by_source_resolution(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_per_stream_type':
|
||||||
|
result = graph.get_total_plays_per_stream_type(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_by_top_10_users':
|
||||||
|
result = graph.get_total_plays_by_top_10_users(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
elif playtype == 'total_plays_by_top_10_platforms':
|
||||||
|
result = graph.get_total_plays_by_top_10_platforms(time_range=time_range, y_axis=y_axis)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.data = result
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to retrieve %s from db' % playtype)
|
||||||
|
|||||||
@@ -19,14 +19,17 @@ Created on Aug 1, 2011
|
|||||||
@author: Michael
|
@author: Michael
|
||||||
'''
|
'''
|
||||||
import platform
|
import platform
|
||||||
import operator
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from plexpy import version
|
from plexpy import version
|
||||||
|
|
||||||
# Identify Our Application
|
# Identify Our Application
|
||||||
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
|
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
|
||||||
|
' ' + platform.release() + ')'
|
||||||
|
|
||||||
|
PLATFORM = platform.system()
|
||||||
|
PLATFORM_VERSION = platform.release()
|
||||||
|
BRANCH = version.PLEXPY_VERSION
|
||||||
|
VERSION_NUMBER = version.PLEXPY_RELEASE_VERSION
|
||||||
|
|
||||||
# Notification Types
|
# Notification Types
|
||||||
NOTIFY_STARTED = 1
|
NOTIFY_STARTED = 1
|
||||||
@@ -39,3 +42,8 @@ notify_strings[NOTIFY_STOPPED] = "Playback stopped"
|
|||||||
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
|
||||||
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
|
||||||
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
|
||||||
|
|
||||||
|
PLATFORM_NAME_OVERRIDES = {'Konvergo': 'Plex Media Player',
|
||||||
|
'Mystery 3': 'Playstation 3',
|
||||||
|
'Mystery 4': 'Playstation 4',
|
||||||
|
'Mystery 5': 'Xbox 360'}
|
||||||
|
|||||||
165
plexpy/config.py
165
plexpy/config.py
@@ -1,4 +1,4 @@
|
|||||||
import plexpy.logger
|
import plexpy.logger
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -26,6 +26,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'PMS_IP': (str, 'PMS', '127.0.0.1'),
|
'PMS_IP': (str, 'PMS', '127.0.0.1'),
|
||||||
'PMS_IS_REMOTE': (int, 'PMS', 0),
|
'PMS_IS_REMOTE': (int, 'PMS', 0),
|
||||||
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
|
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
|
||||||
|
'PMS_NAME': (unicode, 'PMS', ''),
|
||||||
'PMS_PORT': (int, 'PMS', 32400),
|
'PMS_PORT': (int, 'PMS', 32400),
|
||||||
'PMS_TOKEN': (str, 'PMS', ''),
|
'PMS_TOKEN': (str, 'PMS', ''),
|
||||||
'PMS_SSL': (int, 'General', 0),
|
'PMS_SSL': (int, 'General', 0),
|
||||||
@@ -37,9 +38,18 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'API_KEY': (str, 'General', ''),
|
'API_KEY': (str, 'General', ''),
|
||||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
||||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
||||||
|
'BOXCAR_SOUND': (str, 'Boxcar', ''),
|
||||||
'BOXCAR_ON_PLAY': (int, 'Boxcar', 0),
|
'BOXCAR_ON_PLAY': (int, 'Boxcar', 0),
|
||||||
'BOXCAR_ON_STOP': (int, 'Boxcar', 0),
|
'BOXCAR_ON_STOP': (int, 'Boxcar', 0),
|
||||||
|
'BOXCAR_ON_PAUSE': (int, 'Boxcar', 0),
|
||||||
|
'BOXCAR_ON_RESUME': (int, 'Boxcar', 0),
|
||||||
|
'BOXCAR_ON_BUFFER': (int, 'Boxcar', 0),
|
||||||
'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0),
|
'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0),
|
||||||
|
'BOXCAR_ON_CREATED': (int, 'Boxcar', 0),
|
||||||
|
'BOXCAR_ON_EXTDOWN': (int, 'Boxcar', 0),
|
||||||
|
'BOXCAR_ON_INTDOWN': (int, 'Boxcar', 0),
|
||||||
|
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
|
||||||
|
'BUFFER_WAIT': (int, 'Monitoring', 900),
|
||||||
'CACHE_DIR': (str, 'General', ''),
|
'CACHE_DIR': (str, 'General', ''),
|
||||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||||
'CHECK_GITHUB': (int, 'General', 1),
|
'CHECK_GITHUB': (int, 'General', 1),
|
||||||
@@ -58,19 +68,37 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'EMAIL_TLS': (int, 'Email', 0),
|
'EMAIL_TLS': (int, 'Email', 0),
|
||||||
'EMAIL_ON_PLAY': (int, 'Email', 0),
|
'EMAIL_ON_PLAY': (int, 'Email', 0),
|
||||||
'EMAIL_ON_STOP': (int, 'Email', 0),
|
'EMAIL_ON_STOP': (int, 'Email', 0),
|
||||||
|
'EMAIL_ON_PAUSE': (int, 'Email', 0),
|
||||||
|
'EMAIL_ON_RESUME': (int, 'Email', 0),
|
||||||
|
'EMAIL_ON_BUFFER': (int, 'Email', 0),
|
||||||
'EMAIL_ON_WATCHED': (int, 'Email', 0),
|
'EMAIL_ON_WATCHED': (int, 'Email', 0),
|
||||||
|
'EMAIL_ON_CREATED': (int, 'Email', 0),
|
||||||
|
'EMAIL_ON_EXTDOWN': (int, 'Email', 0),
|
||||||
|
'EMAIL_ON_INTDOWN': (int, 'Email', 0),
|
||||||
'ENABLE_HTTPS': (int, 'General', 0),
|
'ENABLE_HTTPS': (int, 'General', 0),
|
||||||
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
'FIRST_RUN_COMPLETE': (int, 'General', 0),
|
||||||
'FREEZE_DB': (int, 'General', 0),
|
'FREEZE_DB': (int, 'General', 0),
|
||||||
'GIT_BRANCH': (str, 'General', 'master'),
|
'GIT_BRANCH': (str, 'General', 'master'),
|
||||||
'GIT_PATH': (str, 'General', ''),
|
'GIT_PATH': (str, 'General', ''),
|
||||||
'GIT_USER': (str, 'General', 'drzoidberg33'),
|
'GIT_USER': (str, 'General', 'drzoidberg33'),
|
||||||
|
'GROUP_HISTORY_TABLES': (int, 'General', 0),
|
||||||
'GROWL_ENABLED': (int, 'Growl', 0),
|
'GROWL_ENABLED': (int, 'Growl', 0),
|
||||||
'GROWL_HOST': (str, 'Growl', ''),
|
'GROWL_HOST': (str, 'Growl', ''),
|
||||||
'GROWL_PASSWORD': (str, 'Growl', ''),
|
'GROWL_PASSWORD': (str, 'Growl', ''),
|
||||||
'GROWL_ON_PLAY': (int, 'Growl', 0),
|
'GROWL_ON_PLAY': (int, 'Growl', 0),
|
||||||
'GROWL_ON_STOP': (int, 'Growl', 0),
|
'GROWL_ON_STOP': (int, 'Growl', 0),
|
||||||
|
'GROWL_ON_PAUSE': (int, 'Growl', 0),
|
||||||
|
'GROWL_ON_RESUME': (int, 'Growl', 0),
|
||||||
|
'GROWL_ON_BUFFER': (int, 'Growl', 0),
|
||||||
'GROWL_ON_WATCHED': (int, 'Growl', 0),
|
'GROWL_ON_WATCHED': (int, 'Growl', 0),
|
||||||
|
'GROWL_ON_CREATED': (int, 'Growl', 0),
|
||||||
|
'GROWL_ON_EXTDOWN': (int, 'Growl', 0),
|
||||||
|
'GROWL_ON_INTDOWN': (int, 'Growl', 0),
|
||||||
|
'HOME_LIBRARY_CARDS': (str, 'General', 'library_statistics_first'),
|
||||||
|
'HOME_STATS_LENGTH': (int, 'General', 30),
|
||||||
|
'HOME_STATS_TYPE': (int, 'General', 0),
|
||||||
|
'HOME_STATS_COUNT': (int, 'General', 5),
|
||||||
|
'HOME_STATS_CARDS': (str, 'General', 'watch_statistics, top_tv, popular_tv, top_movies, popular_movies, top_music, popular_music, top_users, top_platforms, last_watched'),
|
||||||
'HTTPS_CERT': (str, 'General', ''),
|
'HTTPS_CERT': (str, 'General', ''),
|
||||||
'HTTPS_KEY': (str, 'General', ''),
|
'HTTPS_KEY': (str, 'General', ''),
|
||||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||||
@@ -81,80 +109,176 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'HTTP_USERNAME': (str, 'General', ''),
|
'HTTP_USERNAME': (str, 'General', ''),
|
||||||
'INTERFACE': (str, 'General', 'default'),
|
'INTERFACE': (str, 'General', 'default'),
|
||||||
'IP_LOGGING_ENABLE': (int, 'General', 0),
|
'IP_LOGGING_ENABLE': (int, 'General', 0),
|
||||||
|
'IFTTT_KEY': (str, 'IFTTT', ''),
|
||||||
|
'IFTTT_EVENT': (str, 'IFTTT', 'plexpy'),
|
||||||
|
'IFTTT_ENABLED': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_PLAY': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_STOP': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_PAUSE': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_RESUME': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_BUFFER': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_WATCHED': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_CREATED': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_EXTDOWN': (int, 'IFTTT', 0),
|
||||||
|
'IFTTT_ON_INTDOWN': (int, 'IFTTT', 0),
|
||||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||||
'LAUNCH_BROWSER': (int, 'General', 1),
|
'LAUNCH_BROWSER': (int, 'General', 1),
|
||||||
'LOG_DIR': (str, 'General', ''),
|
'LOG_DIR': (str, 'General', ''),
|
||||||
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
|
||||||
|
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||||
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||||
'MOVIE_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
'MOVIE_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
||||||
'MOVIE_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
'MOVIE_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
||||||
|
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
'MUSIC_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
'MUSIC_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||||
'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||||
'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
||||||
'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
||||||
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 0),
|
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
|
||||||
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
|
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
|
||||||
|
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
|
||||||
'NMA_APIKEY': (str, 'NMA', ''),
|
'NMA_APIKEY': (str, 'NMA', ''),
|
||||||
'NMA_ENABLED': (int, 'NMA', 0),
|
'NMA_ENABLED': (int, 'NMA', 0),
|
||||||
'NMA_PRIORITY': (int, 'NMA', 0),
|
'NMA_PRIORITY': (int, 'NMA', 0),
|
||||||
'NMA_ON_PLAY': (int, 'NMA', 0),
|
'NMA_ON_PLAY': (int, 'NMA', 0),
|
||||||
'NMA_ON_STOP': (int, 'NMA', 0),
|
'NMA_ON_STOP': (int, 'NMA', 0),
|
||||||
|
'NMA_ON_PAUSE': (int, 'NMA', 0),
|
||||||
|
'NMA_ON_RESUME': (int, 'NMA', 0),
|
||||||
|
'NMA_ON_BUFFER': (int, 'NMA', 0),
|
||||||
'NMA_ON_WATCHED': (int, 'NMA', 0),
|
'NMA_ON_WATCHED': (int, 'NMA', 0),
|
||||||
|
'NMA_ON_CREATED': (int, 'NMA', 0),
|
||||||
|
'NMA_ON_EXTDOWN': (int, 'NMA', 0),
|
||||||
|
'NMA_ON_INTDOWN': (int, 'NMA', 0),
|
||||||
|
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
|
||||||
|
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
|
||||||
|
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
|
||||||
|
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
|
||||||
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
|
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
|
||||||
'NOTIFY_ON_START_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
|
'NOTIFY_ON_START_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
'NOTIFY_ON_START_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) started playing {title}.'),
|
'NOTIFY_ON_START_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) started playing {title}.'),
|
||||||
'NOTIFY_ON_STOP_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
|
'NOTIFY_ON_STOP_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
'NOTIFY_ON_STOP_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has stopped {title}.'),
|
'NOTIFY_ON_STOP_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has stopped {title}.'),
|
||||||
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
|
'NOTIFY_ON_PAUSE_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
'NOTIFY_ON_WATCHED_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has watched {title}.'),
|
'NOTIFY_ON_PAUSE_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has paused {title}.'),
|
||||||
|
'NOTIFY_ON_RESUME_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
|
'NOTIFY_ON_RESUME_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has resumed {title}.'),
|
||||||
|
'NOTIFY_ON_BUFFER_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
|
'NOTIFY_ON_BUFFER_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) is buffering {title}.'),
|
||||||
|
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
|
'NOTIFY_ON_WATCHED_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has watched {title}.'),
|
||||||
|
'NOTIFY_ON_CREATED_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
|
'NOTIFY_ON_CREATED_BODY_TEXT': (unicode, 'Monitoring', '{title} was recently added to Plex.'),
|
||||||
|
'NOTIFY_ON_EXTDOWN_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
|
'NOTIFY_ON_EXTDOWN_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is down.'),
|
||||||
|
'NOTIFY_ON_INTDOWN_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
|
||||||
|
'NOTIFY_ON_INTDOWN_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is down.'),
|
||||||
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
|
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
|
||||||
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
|
||||||
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
|
||||||
'OSX_NOTIFY_ON_STOP': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ON_STOP': (int, 'OSX_Notify', 0),
|
||||||
|
'OSX_NOTIFY_ON_PAUSE': (int, 'OSX_Notify', 0),
|
||||||
|
'OSX_NOTIFY_ON_RESUME': (int, 'OSX_Notify', 0),
|
||||||
|
'OSX_NOTIFY_ON_BUFFER': (int, 'OSX_Notify', 0),
|
||||||
'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0),
|
'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0),
|
||||||
|
'OSX_NOTIFY_ON_CREATED': (int, 'OSX_Notify', 0),
|
||||||
|
'OSX_NOTIFY_ON_EXTDOWN': (int, 'OSX_Notify', 0),
|
||||||
|
'OSX_NOTIFY_ON_INTDOWN': (int, 'OSX_Notify', 0),
|
||||||
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
|
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
|
||||||
'PLEX_ENABLED': (int, 'Plex', 0),
|
'PLEX_ENABLED': (int, 'Plex', 0),
|
||||||
'PLEX_PASSWORD': (str, 'Plex', ''),
|
'PLEX_PASSWORD': (str, 'Plex', ''),
|
||||||
'PLEX_USERNAME': (str, 'Plex', ''),
|
'PLEX_USERNAME': (str, 'Plex', ''),
|
||||||
'PLEX_ON_PLAY': (int, 'Plex', 0),
|
'PLEX_ON_PLAY': (int, 'Plex', 0),
|
||||||
'PLEX_ON_STOP': (int, 'Plex', 0),
|
'PLEX_ON_STOP': (int, 'Plex', 0),
|
||||||
|
'PLEX_ON_PAUSE': (int, 'Plex', 0),
|
||||||
|
'PLEX_ON_RESUME': (int, 'Plex', 0),
|
||||||
|
'PLEX_ON_BUFFER': (int, 'Plex', 0),
|
||||||
'PLEX_ON_WATCHED': (int, 'Plex', 0),
|
'PLEX_ON_WATCHED': (int, 'Plex', 0),
|
||||||
|
'PLEX_ON_CREATED': (int, 'Plex', 0),
|
||||||
|
'PLEX_ON_EXTDOWN': (int, 'Plex', 0),
|
||||||
|
'PLEX_ON_INTDOWN': (int, 'Plex', 0),
|
||||||
'PROWL_ENABLED': (int, 'Prowl', 0),
|
'PROWL_ENABLED': (int, 'Prowl', 0),
|
||||||
'PROWL_KEYS': (str, 'Prowl', ''),
|
'PROWL_KEYS': (str, 'Prowl', ''),
|
||||||
'PROWL_PRIORITY': (int, 'Prowl', 0),
|
'PROWL_PRIORITY': (int, 'Prowl', 0),
|
||||||
'PROWL_ON_PLAY': (int, 'Prowl', 0),
|
'PROWL_ON_PLAY': (int, 'Prowl', 0),
|
||||||
'PROWL_ON_STOP': (int, 'Prowl', 0),
|
'PROWL_ON_STOP': (int, 'Prowl', 0),
|
||||||
|
'PROWL_ON_PAUSE': (int, 'Prowl', 0),
|
||||||
|
'PROWL_ON_RESUME': (int, 'Prowl', 0),
|
||||||
|
'PROWL_ON_BUFFER': (int, 'Prowl', 0),
|
||||||
'PROWL_ON_WATCHED': (int, 'Prowl', 0),
|
'PROWL_ON_WATCHED': (int, 'Prowl', 0),
|
||||||
|
'PROWL_ON_CREATED': (int, 'Prowl', 0),
|
||||||
|
'PROWL_ON_EXTDOWN': (int, 'Prowl', 0),
|
||||||
|
'PROWL_ON_INTDOWN': (int, 'Prowl', 0),
|
||||||
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
|
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
|
||||||
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
|
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
|
||||||
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
|
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
|
||||||
'PUSHALOT_ON_STOP': (int, 'Pushalot', 0),
|
'PUSHALOT_ON_STOP': (int, 'Pushalot', 0),
|
||||||
|
'PUSHALOT_ON_PAUSE': (int, 'Pushalot', 0),
|
||||||
|
'PUSHALOT_ON_RESUME': (int, 'Pushalot', 0),
|
||||||
|
'PUSHALOT_ON_BUFFER': (int, 'Pushalot', 0),
|
||||||
'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0),
|
'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0),
|
||||||
|
'PUSHALOT_ON_CREATED': (int, 'Pushalot', 0),
|
||||||
|
'PUSHALOT_ON_EXTDOWN': (int, 'Pushalot', 0),
|
||||||
|
'PUSHALOT_ON_INTDOWN': (int, 'Pushalot', 0),
|
||||||
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
|
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
|
||||||
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
|
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
|
||||||
'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''),
|
'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''),
|
||||||
'PUSHBULLET_ENABLED': (int, 'PushBullet', 0),
|
'PUSHBULLET_ENABLED': (int, 'PushBullet', 0),
|
||||||
'PUSHBULLET_ON_PLAY': (int, 'PushBullet', 0),
|
'PUSHBULLET_ON_PLAY': (int, 'PushBullet', 0),
|
||||||
'PUSHBULLET_ON_STOP': (int, 'PushBullet', 0),
|
'PUSHBULLET_ON_STOP': (int, 'PushBullet', 0),
|
||||||
|
'PUSHBULLET_ON_PAUSE': (int, 'PushBullet', 0),
|
||||||
|
'PUSHBULLET_ON_RESUME': (int, 'PushBullet', 0),
|
||||||
|
'PUSHBULLET_ON_BUFFER': (int, 'PushBullet', 0),
|
||||||
'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0),
|
'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0),
|
||||||
|
'PUSHBULLET_ON_CREATED': (int, 'PushBullet', 0),
|
||||||
|
'PUSHBULLET_ON_EXTDOWN': (int, 'PushBullet', 0),
|
||||||
|
'PUSHBULLET_ON_INTDOWN': (int, 'PushBullet', 0),
|
||||||
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
|
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
|
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
|
||||||
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
'PUSHOVER_KEYS': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_SOUND': (str, 'Pushover', ''),
|
||||||
'PUSHOVER_ON_PLAY': (int, 'Pushover', 0),
|
'PUSHOVER_ON_PLAY': (int, 'Pushover', 0),
|
||||||
'PUSHOVER_ON_STOP': (int, 'Pushover', 0),
|
'PUSHOVER_ON_STOP': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_ON_PAUSE': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_ON_RESUME': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_ON_BUFFER': (int, 'Pushover', 0),
|
||||||
'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0),
|
'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_ON_CREATED': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_ON_EXTDOWN': (int, 'Pushover', 0),
|
||||||
|
'PUSHOVER_ON_INTDOWN': (int, 'Pushover', 0),
|
||||||
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
|
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
|
||||||
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
|
||||||
|
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
||||||
|
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
|
||||||
|
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_RESUME': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_BUFFER': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_WATCHED': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_CREATED': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_EXTDOWN': (int, 'Telegram', 0),
|
||||||
|
'TELEGRAM_ON_INTDOWN': (int, 'Telegram', 0),
|
||||||
|
'TV_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0),
|
||||||
'TV_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
'TV_NOTIFY_ON_START': (int, 'Monitoring', 1),
|
||||||
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
|
||||||
'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
|
||||||
'TWITTER_ENABLED': (int, 'Twitter', 0),
|
'TWITTER_ENABLED': (int, 'Twitter', 0),
|
||||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
||||||
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
|
'TWITTER_PREFIX': (str, 'Twitter', 'PlexPy'),
|
||||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
||||||
|
'TWITTER_ON_PLAY': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_STOP': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_PAUSE': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_RESUME': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_BUFFER': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_WATCHED': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_CREATED': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_EXTDOWN': (int, 'Twitter', 0),
|
||||||
|
'TWITTER_ON_INTDOWN': (int, 'Twitter', 0),
|
||||||
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
||||||
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
||||||
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
|
||||||
@@ -164,7 +288,13 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'XBMC_USERNAME': (str, 'XBMC', ''),
|
'XBMC_USERNAME': (str, 'XBMC', ''),
|
||||||
'XBMC_ON_PLAY': (int, 'XBMC', 0),
|
'XBMC_ON_PLAY': (int, 'XBMC', 0),
|
||||||
'XBMC_ON_STOP': (int, 'XBMC', 0),
|
'XBMC_ON_STOP': (int, 'XBMC', 0),
|
||||||
'XBMC_ON_WATCHED': (int, 'XBMC', 0)
|
'XBMC_ON_PAUSE': (int, 'XBMC', 0),
|
||||||
|
'XBMC_ON_RESUME': (int, 'XBMC', 0),
|
||||||
|
'XBMC_ON_BUFFER': (int, 'XBMC', 0),
|
||||||
|
'XBMC_ON_WATCHED': (int, 'XBMC', 0),
|
||||||
|
'XBMC_ON_CREATED': (int, 'XBMC', 0),
|
||||||
|
'XBMC_ON_EXTDOWN': (int, 'XBMC', 0),
|
||||||
|
'XBMC_ON_INTDOWN': (int, 'XBMC', 0)
|
||||||
}
|
}
|
||||||
# pylint:disable=R0902
|
# pylint:disable=R0902
|
||||||
# it might be nice to refactor for fewer instance variables
|
# it might be nice to refactor for fewer instance variables
|
||||||
@@ -177,6 +307,7 @@ class Config(object):
|
|||||||
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
self._config = ConfigObj(self._config_file, encoding='utf-8')
|
||||||
for key in _CONFIG_DEFINITIONS.keys():
|
for key in _CONFIG_DEFINITIONS.keys():
|
||||||
self.check_setting(key)
|
self.check_setting(key)
|
||||||
|
self._upgrade()
|
||||||
|
|
||||||
def _define(self, name):
|
def _define(self, name):
|
||||||
key = name.upper()
|
key = name.upper()
|
||||||
@@ -266,3 +397,17 @@ class Config(object):
|
|||||||
for name, value in kwargs.items():
|
for name, value in kwargs.items():
|
||||||
key, definition_type, section, ini_key, default = self._define(name)
|
key, definition_type, section, ini_key, default = self._define(name)
|
||||||
self._config[section][ini_key] = definition_type(value)
|
self._config[section][ini_key] = definition_type(value)
|
||||||
|
|
||||||
|
def _upgrade(self):
|
||||||
|
"""
|
||||||
|
Upgrades config file from previous verisions and bumps up config version
|
||||||
|
"""
|
||||||
|
if self.CONFIG_VERSION == '0':
|
||||||
|
# Separate out movie and tv notifications
|
||||||
|
if self.MOVIE_NOTIFY_ENABLE == 1:
|
||||||
|
self.TV_NOTIFY_ENABLE = 1
|
||||||
|
# Separate out movie and tv logging
|
||||||
|
if self.VIDEO_LOGGING_ENABLE == 0:
|
||||||
|
self.MOVIE_LOGGING_ENABLE = 0
|
||||||
|
self.TV_LOGGING_ENABLE = 0
|
||||||
|
self.CONFIG_VERSION = '1'
|
||||||
@@ -18,7 +18,10 @@ from plexpy import logger
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
import plexpy
|
import plexpy
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
db_lock = threading.Lock()
|
||||||
|
|
||||||
def drop_session_db():
|
def drop_session_db():
|
||||||
monitor_db = MonitorDatabase()
|
monitor_db = MonitorDatabase()
|
||||||
@@ -55,25 +58,38 @@ class MonitorDatabase(object):
|
|||||||
self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE)
|
self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE)
|
||||||
# 64mb of cache memory, probably need to make it user configurable
|
# 64mb of cache memory, probably need to make it user configurable
|
||||||
self.connection.execute("PRAGMA cache_size=-%s" % (get_cache_size() * 1024))
|
self.connection.execute("PRAGMA cache_size=-%s" % (get_cache_size() * 1024))
|
||||||
self.connection.row_factory = sqlite3.Row
|
self.connection.row_factory = self.dict_factory
|
||||||
|
|
||||||
|
def dict_factory(self, cursor, row):
|
||||||
|
d = {}
|
||||||
|
for idx, col in enumerate(cursor.description):
|
||||||
|
d[col[0]] = row[idx]
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
def action(self, query, args=None, return_last_id=False):
|
def action(self, query, args=None, return_last_id=False):
|
||||||
|
|
||||||
if query is None:
|
if query is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
with db_lock:
|
||||||
sql_result = None
|
sql_result = None
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
while attempts < 5:
|
||||||
try:
|
try:
|
||||||
with self.connection as c:
|
with self.connection as c:
|
||||||
if args is None:
|
if args is None:
|
||||||
sql_result = c.execute(query)
|
sql_result = c.execute(query)
|
||||||
else:
|
else:
|
||||||
sql_result = c.execute(query, args)
|
sql_result = c.execute(query, args)
|
||||||
|
# Our transaction was successful, leave the loop
|
||||||
|
break
|
||||||
|
|
||||||
except sqlite3.OperationalError, e:
|
except sqlite3.OperationalError, e:
|
||||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||||
logger.warn('Database Error: %s', e)
|
logger.warn('Database Error: %s', e)
|
||||||
|
attempts += 1
|
||||||
|
time.sleep(1)
|
||||||
else:
|
else:
|
||||||
logger.error('Database error: %s', e)
|
logger.error('Database error: %s', e)
|
||||||
raise
|
raise
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -70,6 +70,39 @@ class DataTables(object):
|
|||||||
else:
|
else:
|
||||||
grouping = False
|
grouping = False
|
||||||
|
|
||||||
|
# Build join parameters
|
||||||
|
if join_types:
|
||||||
|
counter = 0
|
||||||
|
for join_type in join_types:
|
||||||
|
if join_type.upper() == 'LEFT OUTER JOIN':
|
||||||
|
join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \
|
||||||
|
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
|
||||||
|
elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN':
|
||||||
|
join_item = 'JOIN %s ON %s = %s ' % \
|
||||||
|
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
|
||||||
|
else:
|
||||||
|
join_item = ''
|
||||||
|
|
||||||
|
counter += 1
|
||||||
|
join += join_item
|
||||||
|
|
||||||
|
# Build custom where parameters
|
||||||
|
if custom_where:
|
||||||
|
for w in custom_where:
|
||||||
|
c_where += w[0] + ' = ? AND '
|
||||||
|
|
||||||
|
# The order of our args changes if we are grouping
|
||||||
|
#if grouping:
|
||||||
|
# args.insert(0, w[1])
|
||||||
|
#else:
|
||||||
|
# args.append(w[1])
|
||||||
|
|
||||||
|
# My testing shows that order of args doesn't change
|
||||||
|
args.append(w[1])
|
||||||
|
|
||||||
|
if c_where:
|
||||||
|
c_where = 'WHERE ' + c_where.rstrip(' AND ')
|
||||||
|
|
||||||
# Build ordering
|
# Build ordering
|
||||||
for o in parameters['order']:
|
for o in parameters['order']:
|
||||||
sort_order = ' COLLATE NOCASE'
|
sort_order = ' COLLATE NOCASE'
|
||||||
@@ -119,36 +152,6 @@ class DataTables(object):
|
|||||||
if where:
|
if where:
|
||||||
where = 'WHERE ' + where.rstrip(' OR ')
|
where = 'WHERE ' + where.rstrip(' OR ')
|
||||||
|
|
||||||
# Build join parameters
|
|
||||||
if join_types:
|
|
||||||
counter = 0
|
|
||||||
for join_type in join_types:
|
|
||||||
if join_type.upper() == 'LEFT OUTER JOIN':
|
|
||||||
join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \
|
|
||||||
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
|
|
||||||
elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN':
|
|
||||||
join_item = 'JOIN %s ON %s = %s ' % \
|
|
||||||
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
|
|
||||||
else:
|
|
||||||
join_item = ''
|
|
||||||
|
|
||||||
counter += 1
|
|
||||||
join += join_item
|
|
||||||
|
|
||||||
# Build custom where parameters
|
|
||||||
if custom_where:
|
|
||||||
for w in custom_where:
|
|
||||||
c_where += w[0] + ' = ? AND '
|
|
||||||
|
|
||||||
# The order of our args changes if we are grouping
|
|
||||||
if grouping:
|
|
||||||
args.insert(0, w[1])
|
|
||||||
else:
|
|
||||||
args.append(w[1])
|
|
||||||
|
|
||||||
if c_where:
|
|
||||||
c_where = 'WHERE ' + c_where.rstrip(' AND ')
|
|
||||||
|
|
||||||
# Build our queries
|
# Build our queries
|
||||||
if grouping:
|
if grouping:
|
||||||
if c_where == '':
|
if c_where == '':
|
||||||
@@ -175,12 +178,18 @@ class DataTables(object):
|
|||||||
filtered = self.ssp_db.select(query, args=args)
|
filtered = self.ssp_db.select(query, args=args)
|
||||||
|
|
||||||
# Build grand totals
|
# Build grand totals
|
||||||
totalcount = self.ssp_db.select('SELECT COUNT(id) from %s' % table_name)[0][0]
|
totalcount = self.ssp_db.select('SELECT COUNT(id) as total_count from %s' % table_name)[0]['total_count']
|
||||||
|
|
||||||
# Get draw counter
|
# Get draw counter
|
||||||
draw_counter = int(parameters['draw'])
|
draw_counter = int(parameters['draw'])
|
||||||
|
|
||||||
|
# Paginate results
|
||||||
result = filtered[parameters['start']:(parameters['start'] + parameters['length'])]
|
result = filtered[parameters['start']:(parameters['start'] + parameters['length'])]
|
||||||
|
|
||||||
|
# Sanitize on the way out
|
||||||
|
result = [{k: helpers.sanitize(v) if isinstance(v, basestring) else v for k, v in row.iteritems()}
|
||||||
|
for row in result]
|
||||||
|
|
||||||
output = {'result': result,
|
output = {'result': result,
|
||||||
'draw': draw_counter,
|
'draw': draw_counter,
|
||||||
'filteredCount': len(filtered),
|
'filteredCount': len(filtered),
|
||||||
|
|||||||
1161
plexpy/graphs.py
1161
plexpy/graphs.py
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -92,6 +92,7 @@ def latinToAscii(unicrap):
|
|||||||
}
|
}
|
||||||
|
|
||||||
r = ''
|
r = ''
|
||||||
|
if unicrap:
|
||||||
for i in unicrap:
|
for i in unicrap:
|
||||||
if ord(i) in xlate:
|
if ord(i) in xlate:
|
||||||
r += xlate[ord(i)]
|
r += xlate[ord(i)]
|
||||||
@@ -99,6 +100,7 @@ def latinToAscii(unicrap):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
r += str(i)
|
r += str(i)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
@@ -144,6 +146,31 @@ def now():
|
|||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
return now.strftime("%Y-%m-%d %H:%M:%S")
|
return now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
def human_duration(s):
|
||||||
|
|
||||||
|
hd = ''
|
||||||
|
|
||||||
|
if str(s).isdigit():
|
||||||
|
d = int(s / 84600)
|
||||||
|
h = int((s % 84600) / 3600)
|
||||||
|
m = int(((s % 84600) % 3600) / 60)
|
||||||
|
s = int(((s % 84600) % 3600) % 60)
|
||||||
|
|
||||||
|
hd_list = []
|
||||||
|
if d > 0:
|
||||||
|
hd_list.append(str(d) + ' days')
|
||||||
|
if h > 0:
|
||||||
|
hd_list.append(str(h) + ' hrs')
|
||||||
|
if m > 0:
|
||||||
|
hd_list.append(str(m) + ' mins')
|
||||||
|
if s > 0:
|
||||||
|
hd_list.append(str(s) + ' secs')
|
||||||
|
|
||||||
|
hd = ' '.join(hd_list)
|
||||||
|
|
||||||
|
return hd
|
||||||
|
else:
|
||||||
|
return hd
|
||||||
|
|
||||||
def get_age(date):
|
def get_age(date):
|
||||||
|
|
||||||
@@ -403,3 +430,9 @@ def process_json_kwargs(json_kwargs):
|
|||||||
params = json.loads(json_kwargs)
|
params = json.loads(json_kwargs)
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def sanitize(string):
|
||||||
|
if string:
|
||||||
|
return unicode(string).replace('<','<').replace('>','>')
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
@@ -14,12 +17,11 @@
|
|||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger, helpers
|
from plexpy import logger, helpers
|
||||||
|
|
||||||
from httplib import HTTPSConnection
|
from httplib import HTTPSConnection
|
||||||
from httplib import HTTPConnection
|
from httplib import HTTPConnection
|
||||||
|
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
|
|
||||||
class HTTPHandler(object):
|
class HTTPHandler(object):
|
||||||
"""
|
"""
|
||||||
Retrieve data from Plex Server
|
Retrieve data from Plex Server
|
||||||
@@ -52,7 +54,7 @@ class HTTPHandler(object):
|
|||||||
|
|
||||||
if uri:
|
if uri:
|
||||||
if proto.upper() == 'HTTPS':
|
if proto.upper() == 'HTTPS':
|
||||||
if not self.ssl_verify:
|
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
|
||||||
context = ssl._create_unverified_context()
|
context = ssl._create_unverified_context()
|
||||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=10, context=context)
|
handler = HTTPSConnection(host=self.host, port=self.port, timeout=10, context=context)
|
||||||
logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
|
logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
|
||||||
@@ -81,12 +83,14 @@ class HTTPHandler(object):
|
|||||||
logger.warn(u"Failed to access uri endpoint %s with error %s" % (uri, e))
|
logger.warn(u"Failed to access uri endpoint %s with error %s" % (uri, e))
|
||||||
return None
|
return None
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only?" % uri)
|
logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (uri, e))
|
||||||
return None
|
return None
|
||||||
except:
|
except:
|
||||||
logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % uri)
|
logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % uri)
|
||||||
|
return None
|
||||||
|
|
||||||
if request_status == 200:
|
if request_status == 200:
|
||||||
|
try:
|
||||||
if output_format == 'dict':
|
if output_format == 'dict':
|
||||||
output = helpers.convert_xml_to_dict(request_content)
|
output = helpers.convert_xml_to_dict(request_content)
|
||||||
elif output_format == 'json':
|
elif output_format == 'json':
|
||||||
@@ -100,6 +104,11 @@ class HTTPHandler(object):
|
|||||||
return output, content_type
|
return output, content_type
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(u"Failed format response from uri %s to %s error %s" % (uri, output_format, e))
|
||||||
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status))
|
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status))
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,348 +0,0 @@
|
|||||||
# This file is part of PlexPy.
|
|
||||||
#
|
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# PlexPy is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from plexpy import logger, pmsconnect, notification_handler, log_reader, common, database, helpers
|
|
||||||
|
|
||||||
import threading
|
|
||||||
import plexpy
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
monitor_lock = threading.Lock()
|
|
||||||
|
|
||||||
def check_active_sessions():
|
|
||||||
|
|
||||||
with monitor_lock:
|
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
|
||||||
session_list = pms_connect.get_current_activity()
|
|
||||||
monitor_db = database.MonitorDatabase()
|
|
||||||
monitor_process = MonitorProcessing()
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Checking for active streams.")
|
|
||||||
|
|
||||||
if session_list:
|
|
||||||
media_container = session_list['sessions']
|
|
||||||
|
|
||||||
# Check our temp table for what we must do with the new streams
|
|
||||||
db_streams = monitor_db.select('SELECT started, session_key, rating_key, media_type, title, parent_title, '
|
|
||||||
'grandparent_title, user_id, user, friendly_name, ip_address, player, '
|
|
||||||
'platform, machine_id, parent_rating_key, grandparent_rating_key, state, '
|
|
||||||
'view_offset, duration, video_decision, audio_decision, width, height, '
|
|
||||||
'container, video_codec, audio_codec, bitrate, video_resolution, '
|
|
||||||
'video_framerate, aspect_ratio, audio_channels, transcode_protocol, '
|
|
||||||
'transcode_container, transcode_video_codec, transcode_audio_codec, '
|
|
||||||
'transcode_audio_channels, transcode_width, transcode_height, paused_counter '
|
|
||||||
'FROM sessions')
|
|
||||||
for stream in db_streams:
|
|
||||||
if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key'])
|
|
||||||
for d in media_container):
|
|
||||||
# The user's session is still active
|
|
||||||
for session in media_container:
|
|
||||||
if session['session_key'] == str(stream['session_key']) and \
|
|
||||||
session['rating_key'] == str(stream['rating_key']):
|
|
||||||
# The user is still playing the same media item
|
|
||||||
# Here we can check the play states
|
|
||||||
if session['state'] != stream['state']:
|
|
||||||
if session['state'] == 'paused':
|
|
||||||
# Push any notifications -
|
|
||||||
# Push it on it's own thread so we don't hold up our db actions
|
|
||||||
threading.Thread(target=notification_handler.notify,
|
|
||||||
kwargs=dict(stream_data=stream, notify_action='pause')).start()
|
|
||||||
if stream['state'] == 'paused':
|
|
||||||
# The stream is still paused so we need to increment the paused_counter
|
|
||||||
# Using the set config parameter as the interval, probably not the most accurate but
|
|
||||||
# it will have to do for now.
|
|
||||||
paused_counter = int(stream['paused_counter']) + plexpy.CONFIG.MONITORING_INTERVAL
|
|
||||||
monitor_db.action('UPDATE sessions SET paused_counter = ? '
|
|
||||||
'WHERE session_key = ? AND rating_key = ?',
|
|
||||||
[paused_counter, stream['session_key'], stream['rating_key']])
|
|
||||||
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
|
||||||
# Don't trigger if state is buffer as some clients push the progress to the end when
|
|
||||||
# buffering on start.
|
|
||||||
if session['progress'] and session['duration'] and session['state'] != 'buffering':
|
|
||||||
if helpers.get_percent(session['progress'],
|
|
||||||
session['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
|
||||||
# Push any notifications -
|
|
||||||
# Push it on it's own thread so we don't hold up our db actions
|
|
||||||
threading.Thread(target=notification_handler.notify,
|
|
||||||
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# The user has stopped playing a stream
|
|
||||||
logger.debug(u"PlexPy Monitor :: Removing sessionKey %s ratingKey %s from session queue"
|
|
||||||
% (stream['session_key'], stream['rating_key']))
|
|
||||||
monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?',
|
|
||||||
[stream['session_key'], stream['rating_key']])
|
|
||||||
|
|
||||||
# Check if the user has reached the offset in the media we defined as the "watched" percent
|
|
||||||
if stream['view_offset'] and stream['duration']:
|
|
||||||
if helpers.get_percent(stream['view_offset'],
|
|
||||||
stream['duration']) > plexpy.CONFIG.NOTIFY_WATCHED_PERCENT:
|
|
||||||
# Push any notifications -
|
|
||||||
# Push it on it's own thread so we don't hold up our db actions
|
|
||||||
threading.Thread(target=notification_handler.notify,
|
|
||||||
kwargs=dict(stream_data=stream, notify_action='watched')).start()
|
|
||||||
|
|
||||||
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
|
||||||
threading.Thread(target=notification_handler.notify,
|
|
||||||
kwargs=dict(stream_data=stream, notify_action='stop')).start()
|
|
||||||
|
|
||||||
# Write the item history on playback stop
|
|
||||||
monitor_process.write_session_history(session=stream)
|
|
||||||
|
|
||||||
# Process the newly received session data
|
|
||||||
for session in media_container:
|
|
||||||
monitor_process.write_session(session)
|
|
||||||
else:
|
|
||||||
logger.debug(u"PlexPy Monitor :: Unable to read session list.")
|
|
||||||
|
|
||||||
|
|
||||||
class MonitorProcessing(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.db = database.MonitorDatabase()
|
|
||||||
|
|
||||||
def write_session(self, session=None):
|
|
||||||
|
|
||||||
values = {'session_key': session['session_key'],
|
|
||||||
'rating_key': session['rating_key'],
|
|
||||||
'media_type': session['type'],
|
|
||||||
'state': session['state'],
|
|
||||||
'user_id': session['user_id'],
|
|
||||||
'user': session['user'],
|
|
||||||
'machine_id': session['machine_id'],
|
|
||||||
'title': session['title'],
|
|
||||||
'parent_title': session['parent_title'],
|
|
||||||
'grandparent_title': session['grandparent_title'],
|
|
||||||
'friendly_name': session['friendly_name'],
|
|
||||||
'player': session['player'],
|
|
||||||
'platform': session['platform'],
|
|
||||||
'parent_rating_key': session['parent_rating_key'],
|
|
||||||
'grandparent_rating_key': session['grandparent_rating_key'],
|
|
||||||
'view_offset': session['progress'],
|
|
||||||
'duration': session['duration'],
|
|
||||||
'video_decision': session['video_decision'],
|
|
||||||
'audio_decision': session['audio_decision'],
|
|
||||||
'width': session['width'],
|
|
||||||
'height': session['height'],
|
|
||||||
'container': session['container'],
|
|
||||||
'video_codec': session['video_codec'],
|
|
||||||
'audio_codec': session['audio_codec'],
|
|
||||||
'bitrate': session['bitrate'],
|
|
||||||
'video_resolution': session['video_resolution'],
|
|
||||||
'video_framerate': session['video_framerate'],
|
|
||||||
'aspect_ratio': session['aspect_ratio'],
|
|
||||||
'audio_channels': session['audio_channels'],
|
|
||||||
'transcode_protocol': session['transcode_protocol'],
|
|
||||||
'transcode_container': session['transcode_container'],
|
|
||||||
'transcode_video_codec': session['transcode_video_codec'],
|
|
||||||
'transcode_audio_codec': session['transcode_audio_codec'],
|
|
||||||
'transcode_audio_channels': session['transcode_audio_channels'],
|
|
||||||
'transcode_width': session['transcode_width'],
|
|
||||||
'transcode_height': session['transcode_height']
|
|
||||||
}
|
|
||||||
|
|
||||||
keys = {'session_key': session['session_key'],
|
|
||||||
'rating_key': session['rating_key']}
|
|
||||||
|
|
||||||
result = self.db.upsert('sessions', values, keys)
|
|
||||||
|
|
||||||
if result == 'insert':
|
|
||||||
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
|
|
||||||
threading.Thread(target=notification_handler.notify,
|
|
||||||
kwargs=dict(stream_data=values,notify_action='play')).start()
|
|
||||||
|
|
||||||
started = int(time.time())
|
|
||||||
|
|
||||||
# Try and grab IP address from logs
|
|
||||||
if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER:
|
|
||||||
ip_address = self.find_session_ip(rating_key=session['rating_key'],
|
|
||||||
machine_id=session['machine_id'])
|
|
||||||
else:
|
|
||||||
ip_address = None
|
|
||||||
|
|
||||||
timestamp = {'started': started,
|
|
||||||
'ip_address': ip_address}
|
|
||||||
|
|
||||||
# If it's our first write then time stamp it.
|
|
||||||
self.db.upsert('sessions', timestamp, keys)
|
|
||||||
|
|
||||||
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
|
|
||||||
|
|
||||||
if session:
|
|
||||||
logging_enabled = False
|
|
||||||
|
|
||||||
if is_import:
|
|
||||||
if str(session['stopped']).isdigit():
|
|
||||||
stopped = session['stopped']
|
|
||||||
else:
|
|
||||||
stopped = int(time.time())
|
|
||||||
else:
|
|
||||||
stopped = int(time.time())
|
|
||||||
|
|
||||||
if plexpy.CONFIG.VIDEO_LOGGING_ENABLE and \
|
|
||||||
(session['media_type'] == 'movie' or session['media_type'] == 'episode'):
|
|
||||||
logging_enabled = True
|
|
||||||
elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and \
|
|
||||||
session['media_type'] == 'track':
|
|
||||||
logging_enabled = True
|
|
||||||
else:
|
|
||||||
logger.debug(u"PlexPy Monitor :: ratingKey %s not logged. Does not meet logging criteria. "
|
|
||||||
u"Media type is '%s'" % (session['rating_key'], session['media_type']))
|
|
||||||
|
|
||||||
if plexpy.CONFIG.LOGGING_IGNORE_INTERVAL and not is_import:
|
|
||||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
|
||||||
(int(stopped) - session['started'] < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)):
|
|
||||||
logging_enabled = False
|
|
||||||
logger.debug(u"PlexPy Monitor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
|
||||||
u"seconds, so we're not logging it." %
|
|
||||||
(session['rating_key'], str(int(stopped) - session['started']),
|
|
||||||
plexpy.CONFIG.LOGGING_IGNORE_INTERVAL))
|
|
||||||
elif is_import and import_ignore_interval:
|
|
||||||
if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \
|
|
||||||
(int(stopped) - session['started'] < int(import_ignore_interval)):
|
|
||||||
logging_enabled = False
|
|
||||||
logger.debug(u"PlexPy Monitor :: Play duration for ratingKey %s is %s secs which is less than %s "
|
|
||||||
u"seconds, so we're not logging it." %
|
|
||||||
(session['rating_key'], str(int(stopped) - session['started']),
|
|
||||||
import_ignore_interval))
|
|
||||||
|
|
||||||
if logging_enabled:
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Attempting to write to session_history table...")
|
|
||||||
query = 'INSERT INTO session_history (started, stopped, rating_key, parent_rating_key, ' \
|
|
||||||
'grandparent_rating_key, media_type, user_id, user, ip_address, paused_counter, player, ' \
|
|
||||||
'platform, machine_id, view_offset) VALUES ' \
|
|
||||||
'(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
||||||
|
|
||||||
args = [session['started'], stopped, session['rating_key'], session['parent_rating_key'],
|
|
||||||
session['grandparent_rating_key'], session['media_type'], session['user_id'], session['user'],
|
|
||||||
session['ip_address'], session['paused_counter'], session['player'], session['platform'],
|
|
||||||
session['machine_id'], session['view_offset']]
|
|
||||||
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Writing session_history transaction...")
|
|
||||||
self.db.action(query=query, args=args)
|
|
||||||
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Successfully written history item, last id for session_history is %s"
|
|
||||||
# % last_id)
|
|
||||||
|
|
||||||
# Write the session_history_media_info table
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Attempting to write to session_history_media_info table...")
|
|
||||||
query = 'INSERT INTO session_history_media_info (id, rating_key, video_decision, audio_decision, ' \
|
|
||||||
'duration, width, height, container, video_codec, audio_codec, bitrate, video_resolution, ' \
|
|
||||||
'video_framerate, aspect_ratio, audio_channels, transcode_protocol, transcode_container, ' \
|
|
||||||
'transcode_video_codec, transcode_audio_codec, transcode_audio_channels, transcode_width, ' \
|
|
||||||
'transcode_height) VALUES ' \
|
|
||||||
'(last_insert_rowid(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
||||||
|
|
||||||
args = [session['rating_key'], session['video_decision'], session['audio_decision'],
|
|
||||||
session['duration'], session['width'], session['height'], session['container'],
|
|
||||||
session['video_codec'], session['audio_codec'], session['bitrate'],
|
|
||||||
session['video_resolution'], session['video_framerate'], session['aspect_ratio'],
|
|
||||||
session['audio_channels'], session['transcode_protocol'], session['transcode_container'],
|
|
||||||
session['transcode_video_codec'], session['transcode_audio_codec'],
|
|
||||||
session['transcode_audio_channels'], session['transcode_width'], session['transcode_height']]
|
|
||||||
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Writing session_history_media_info transaction...")
|
|
||||||
self.db.action(query=query, args=args)
|
|
||||||
|
|
||||||
if not is_import:
|
|
||||||
logger.debug(u"PlexPy Monitor :: Fetching metadata for item ratingKey %s" % session['rating_key'])
|
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
|
||||||
result = pms_connect.get_metadata_details(rating_key=str(session['rating_key']))
|
|
||||||
metadata = result['metadata']
|
|
||||||
else:
|
|
||||||
metadata = import_metadata
|
|
||||||
|
|
||||||
# Write the session_history_metadata table
|
|
||||||
directors = ";".join(metadata['directors'])
|
|
||||||
writers = ";".join(metadata['writers'])
|
|
||||||
actors = ";".join(metadata['actors'])
|
|
||||||
genres = ";".join(metadata['genres'])
|
|
||||||
|
|
||||||
# Build media item title
|
|
||||||
if session['media_type'] == 'episode' or session['media_type'] == 'track':
|
|
||||||
full_title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
|
||||||
elif session['media_type'] == 'movie':
|
|
||||||
full_title = metadata['title']
|
|
||||||
else:
|
|
||||||
full_title = metadata['title']
|
|
||||||
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Attempting to write to session_history_metadata table...")
|
|
||||||
query = 'INSERT INTO session_history_metadata (id, rating_key, parent_rating_key, ' \
|
|
||||||
'grandparent_rating_key, title, parent_title, grandparent_title, full_title, media_index, ' \
|
|
||||||
'parent_media_index, thumb, parent_thumb, grandparent_thumb, art, media_type, year, ' \
|
|
||||||
'originally_available_at, added_at, updated_at, last_viewed_at, content_rating, summary, ' \
|
|
||||||
'rating, duration, guid, directors, writers, actors, genres, studio) VALUES ' \
|
|
||||||
'(last_insert_rowid(), ' \
|
|
||||||
'?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
||||||
|
|
||||||
args = [session['rating_key'], session['parent_rating_key'], session['grandparent_rating_key'],
|
|
||||||
session['title'], session['parent_title'], session['grandparent_title'], full_title,
|
|
||||||
metadata['index'], metadata['parent_index'], metadata['thumb'], metadata['parent_thumb'],
|
|
||||||
metadata['grandparent_thumb'], metadata['art'], session['media_type'], metadata['year'],
|
|
||||||
metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
|
|
||||||
metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['rating'],
|
|
||||||
metadata['duration'], metadata['guid'], directors, writers, actors, genres, metadata['studio']]
|
|
||||||
|
|
||||||
# logger.debug(u"PlexPy Monitor :: Writing session_history_metadata transaction...")
|
|
||||||
self.db.action(query=query, args=args)
|
|
||||||
|
|
||||||
def find_session_ip(self, rating_key=None, machine_id=None):
|
|
||||||
|
|
||||||
logger.debug(u"PlexPy Monitor :: Requesting log lines...")
|
|
||||||
log_lines = log_reader.get_log_tail(window=5000, parsed=False)
|
|
||||||
|
|
||||||
rating_key_line = 'ratingKey=' + rating_key
|
|
||||||
rating_key_line_2 = 'metadata%2F' + rating_key
|
|
||||||
machine_id_line = 'session=' + machine_id
|
|
||||||
|
|
||||||
for line in reversed(log_lines):
|
|
||||||
# We're good if we find a line with both machine id and rating key
|
|
||||||
# This is usually when there is a transcode session
|
|
||||||
if machine_id_line in line and (rating_key_line in line or rating_key_line_2 in line):
|
|
||||||
# Currently only checking for ipv4 addresses
|
|
||||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line)
|
|
||||||
if ipv4:
|
|
||||||
# The logged IP will always be the first match and we don't want localhost entries
|
|
||||||
if ipv4[0] != '127.0.0.1':
|
|
||||||
logger.debug(u"PlexPy Monitor :: Matched IP address (%s) for stream ratingKey %s "
|
|
||||||
u"and machineIdentifier %s."
|
|
||||||
% (ipv4[0], rating_key, machine_id))
|
|
||||||
return ipv4[0]
|
|
||||||
|
|
||||||
logger.debug(u"PlexPy Monitor :: Unable to find IP address on first pass. "
|
|
||||||
u"Attempting fallback check in 5 seconds...")
|
|
||||||
|
|
||||||
# Wait for the log to catch up and read in new lines
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
logger.debug(u"PlexPy Monitor :: Requesting log lines...")
|
|
||||||
log_lines = log_reader.get_log_tail(window=5000, parsed=False)
|
|
||||||
|
|
||||||
for line in reversed(log_lines):
|
|
||||||
if 'GET /:/timeline' in line and (rating_key_line in line or rating_key_line_2 in line):
|
|
||||||
# Currently only checking for ipv4 addresses
|
|
||||||
# This method can return the wrong IP address if more than one user
|
|
||||||
# starts watching the same media item around the same time.
|
|
||||||
ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}', line)
|
|
||||||
if ipv4:
|
|
||||||
# The logged IP will always be the first match and we don't want localhost entries
|
|
||||||
if ipv4[0] != '127.0.0.1':
|
|
||||||
logger.debug(u"PlexPy Monitor :: Matched IP address (%s) for stream ratingKey %s." %
|
|
||||||
(ipv4[0], rating_key))
|
|
||||||
return ipv4[0]
|
|
||||||
|
|
||||||
logger.debug(u"PlexPy Monitor :: Unable to find IP address on fallback search. Not logging IP address.")
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -13,25 +13,27 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger, config, notifiers, database
|
from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
def notify(stream_data=None, notify_action=None):
|
def notify(stream_data=None, notify_action=None):
|
||||||
from plexpy import datafactory
|
from plexpy import users
|
||||||
|
|
||||||
if stream_data and notify_action:
|
if stream_data and notify_action:
|
||||||
# Check if notifications enabled for user
|
# Check if notifications enabled for user
|
||||||
data_factory = datafactory.DataFactory()
|
user_data = users.Users()
|
||||||
user_details = data_factory.get_user_friendly_name(user=stream_data['user'])
|
user_details = user_data.get_user_friendly_name(user=stream_data['user'])
|
||||||
|
|
||||||
if not user_details['do_notify']:
|
if not user_details['do_notify']:
|
||||||
return
|
return
|
||||||
|
|
||||||
if stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode':
|
if (stream_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
|
||||||
if plexpy.CONFIG.MOVIE_NOTIFY_ENABLE or plexpy.CONFIG.TV_NOTIFY_ENABLE:
|
or (stream_data['media_type'] == 'episode' and plexpy.CONFIG.TV_NOTIFY_ENABLE):
|
||||||
|
|
||||||
|
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
|
||||||
|
|
||||||
for agent in notifiers.available_notification_agents():
|
for agent in notifiers.available_notification_agents():
|
||||||
if agent['on_play'] and notify_action == 'play':
|
if agent['on_play'] and notify_action == 'play':
|
||||||
@@ -41,16 +43,46 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1])
|
body=notify_strings[1])
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state='play', agent_info=agent)
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
elif agent['on_stop'] and notify_action == 'stop':
|
elif agent['on_stop'] and notify_action == 'stop' \
|
||||||
|
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT):
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
notifiers.send_notification(config_id=agent['id'],
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1])
|
body=notify_strings[1])
|
||||||
|
|
||||||
set_notify_state(session=stream_data, state='stop', agent_info=agent)
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif agent['on_pause'] and notify_action == 'pause' \
|
||||||
|
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
|
||||||
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif agent['on_resume'] and notify_action == 'resume' \
|
||||||
|
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
|
||||||
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif agent['on_buffer'] and notify_action == 'buffer':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
|
||||||
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
elif agent['on_watched'] and notify_action == 'watched':
|
elif agent['on_watched'] and notify_action == 'watched':
|
||||||
# Get the current states for notifications from our db
|
# Get the current states for notifications from our db
|
||||||
@@ -64,7 +96,7 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1])
|
body=notify_strings[1])
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state='watched', agent_info=agent)
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Check in our notify log if the notification has already been sent
|
# Check in our notify log if the notification has already been sent
|
||||||
@@ -76,10 +108,9 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1])
|
body=notify_strings[1])
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state='watched', agent_info=agent)
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
elif stream_data['media_type'] == 'track':
|
elif (stream_data['media_type'] == 'track' and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
|
||||||
if plexpy.CONFIG.MUSIC_NOTIFY_ENABLE:
|
|
||||||
|
|
||||||
for agent in notifiers.available_notification_agents():
|
for agent in notifiers.available_notification_agents():
|
||||||
if agent['on_play'] and notify_action == 'play':
|
if agent['on_play'] and notify_action == 'play':
|
||||||
@@ -89,7 +120,7 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1])
|
body=notify_strings[1])
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state='play', agent_info=agent)
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
elif agent['on_stop'] and notify_action == 'stop':
|
elif agent['on_stop'] and notify_action == 'stop':
|
||||||
# Build and send notification
|
# Build and send notification
|
||||||
@@ -98,19 +129,83 @@ def notify(stream_data=None, notify_action=None):
|
|||||||
subject=notify_strings[0],
|
subject=notify_strings[0],
|
||||||
body=notify_strings[1])
|
body=notify_strings[1])
|
||||||
# Set the notification state in the db
|
# Set the notification state in the db
|
||||||
set_notify_state(session=stream_data, state='stop', agent_info=agent)
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif agent['on_pause'] and notify_action == 'pause':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif agent['on_resume'] and notify_action == 'resume':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif agent['on_buffer'] and notify_action == 'buffer':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
elif stream_data['media_type'] == 'clip':
|
elif stream_data['media_type'] == 'clip':
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
logger.debug(u"PlexPy Notifier :: Notify called with unsupported media type.")
|
#logger.debug(u"PlexPy Notifier :: Notify called with unsupported media type.")
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.")
|
logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.")
|
||||||
|
|
||||||
|
|
||||||
|
def notify_timeline(timeline_data=None, notify_action=None):
|
||||||
|
if timeline_data and notify_action:
|
||||||
|
if (timeline_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
|
||||||
|
or ((timeline_data['media_type'] == 'show' or timeline_data['media_type'] == 'episode') \
|
||||||
|
and plexpy.CONFIG.TV_NOTIFY_ENABLE) \
|
||||||
|
or ((timeline_data['media_type'] == 'artist' or timeline_data['media_type'] == 'track') \
|
||||||
|
and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
|
||||||
|
|
||||||
|
for agent in notifiers.available_notification_agents():
|
||||||
|
if agent['on_created'] and notify_action == 'created':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_notify_text(timeline=timeline_data, state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
# Set the notification state in the db
|
||||||
|
set_notify_state(session=timeline_data, state=notify_action, agent_info=agent)
|
||||||
|
|
||||||
|
elif not timeline_data and notify_action:
|
||||||
|
for agent in notifiers.available_notification_agents():
|
||||||
|
if agent['on_extdown'] and notify_action == 'extdown':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_server_notify_text(state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
if agent['on_intdown'] and notify_action == 'intdown':
|
||||||
|
# Build and send notification
|
||||||
|
notify_strings = build_server_notify_text(state=notify_action)
|
||||||
|
notifiers.send_notification(config_id=agent['id'],
|
||||||
|
subject=notify_strings[0],
|
||||||
|
body=notify_strings[1])
|
||||||
|
else:
|
||||||
|
logger.debug(u"PlexPy Notifier :: Notify timeline called but incomplete data received.")
|
||||||
|
|
||||||
|
|
||||||
def get_notify_state(session):
|
def get_notify_state(session):
|
||||||
monitor_db = database.MonitorDatabase()
|
monitor_db = database.MonitorDatabase()
|
||||||
result = monitor_db.select('SELECT on_play, on_stop, on_watched, agent_id '
|
result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id '
|
||||||
'FROM notify_log '
|
'FROM notify_log '
|
||||||
'WHERE session_key = ? '
|
'WHERE session_key = ? '
|
||||||
'AND rating_key = ? '
|
'AND rating_key = ? '
|
||||||
@@ -121,12 +216,31 @@ def get_notify_state(session):
|
|||||||
for item in result:
|
for item in result:
|
||||||
notify_state = {'on_play': item[0],
|
notify_state = {'on_play': item[0],
|
||||||
'on_stop': item[1],
|
'on_stop': item[1],
|
||||||
'on_watched': item[2],
|
'on_pause': item[2],
|
||||||
'agent_id': item[3]}
|
'on_resume': item[3],
|
||||||
|
'on_buffer': item[4],
|
||||||
|
'on_watched': item[5],
|
||||||
|
'agent_id': item[6]}
|
||||||
notify_states.append(notify_state)
|
notify_states.append(notify_state)
|
||||||
|
|
||||||
return notify_states
|
return notify_states
|
||||||
|
|
||||||
|
def get_notify_state_timeline(timeline):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
result = monitor_db.select('SELECT on_created, agent_id '
|
||||||
|
'FROM notify_log '
|
||||||
|
'WHERE rating_key = ? '
|
||||||
|
'ORDER BY id DESC',
|
||||||
|
args=[timeline['rating_key']])
|
||||||
|
notify_states = []
|
||||||
|
for item in result:
|
||||||
|
notify_state = {'on_created': item[0],
|
||||||
|
'agent_id': item[1]}
|
||||||
|
notify_states.append(notify_state)
|
||||||
|
|
||||||
|
return notify_states
|
||||||
|
|
||||||
|
|
||||||
def set_notify_state(session, state, agent_info):
|
def set_notify_state(session, state, agent_info):
|
||||||
|
|
||||||
if session and state and agent_info:
|
if session and state and agent_info:
|
||||||
@@ -136,11 +250,24 @@ def set_notify_state(session, state, agent_info):
|
|||||||
values = {'on_play': int(time.time())}
|
values = {'on_play': int(time.time())}
|
||||||
elif state == 'stop':
|
elif state == 'stop':
|
||||||
values = {'on_stop': int(time.time())}
|
values = {'on_stop': int(time.time())}
|
||||||
|
elif state == 'pause':
|
||||||
|
values = {'on_pause': int(time.time())}
|
||||||
|
elif state == 'resume':
|
||||||
|
values = {'on_resume': int(time.time())}
|
||||||
|
elif state == 'buffer':
|
||||||
|
values = {'on_buffer': int(time.time())}
|
||||||
elif state == 'watched':
|
elif state == 'watched':
|
||||||
values = {'on_watched': int(time.time())}
|
values = {'on_watched': int(time.time())}
|
||||||
|
elif state == 'created':
|
||||||
|
values = {'on_created': int(time.time())}
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if state == 'created':
|
||||||
|
keys = {'rating_key': session['rating_key'],
|
||||||
|
'agent_id': agent_info['id'],
|
||||||
|
'agent_name': agent_info['name']}
|
||||||
|
else:
|
||||||
keys = {'session_key': session['session_key'],
|
keys = {'session_key': session['session_key'],
|
||||||
'rating_key': session['rating_key'],
|
'rating_key': session['rating_key'],
|
||||||
'user_id': session['user_id'],
|
'user_id': session['user_id'],
|
||||||
@@ -152,112 +279,168 @@ def set_notify_state(session, state, agent_info):
|
|||||||
else:
|
else:
|
||||||
logger.error('PlexPy Notifier :: Unable to set notify state.')
|
logger.error('PlexPy Notifier :: Unable to set notify state.')
|
||||||
|
|
||||||
def build_notify_text(session, state):
|
|
||||||
from plexpy import pmsconnect, helpers
|
def build_notify_text(session=None, timeline=None, state=None):
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Get the server name
|
# Get the server name
|
||||||
pms_connect = pmsconnect.PmsConnect()
|
server_name = plexpy.CONFIG.PMS_NAME
|
||||||
server_name = pms_connect.get_server_pref(pref='FriendlyName')
|
|
||||||
|
# Get the server uptime
|
||||||
|
plex_tv = plextv.PlexTV()
|
||||||
|
server_times = plex_tv.get_server_times()
|
||||||
|
|
||||||
|
if server_times:
|
||||||
|
updated_at = server_times[0]['updated_at']
|
||||||
|
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
|
||||||
|
else:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to retrieve server uptime.")
|
||||||
|
server_uptime = 'N/A'
|
||||||
|
|
||||||
# Get metadata feed for item
|
# Get metadata feed for item
|
||||||
metadata = pms_connect.get_metadata_details(rating_key=session['rating_key'])
|
if session:
|
||||||
|
rating_key = session['rating_key']
|
||||||
|
elif timeline:
|
||||||
|
rating_key = timeline['rating_key']
|
||||||
|
|
||||||
if metadata:
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
item_metadata = metadata['metadata']
|
metadata_list = pms_connect.get_metadata_details(rating_key=rating_key)
|
||||||
|
|
||||||
|
if metadata_list:
|
||||||
|
metadata = metadata_list['metadata']
|
||||||
else:
|
else:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to retrieve metadata for rating_key %s" % str(session['rating_key']))
|
logger.error(u"PlexPy Notifier :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Check for exclusion tags
|
# Check for exclusion tags
|
||||||
if session['media_type'] == 'episode':
|
if metadata['media_type'] == 'movie':
|
||||||
# Regex pattern to remove the text in the tags we don't want
|
# Regex pattern to remove the text in the tags we don't want
|
||||||
pattern = re.compile('<movie>[^>]+.</movie>|<music>[^>]+.</music>', re.IGNORECASE)
|
pattern = re.compile('<tv>[^>]+.</tv>|<music>[^>]+.</music>', re.IGNORECASE|re.DOTALL)
|
||||||
|
elif metadata['media_type'] == 'show' or metadata['media_type'] == 'episode':
|
||||||
# Remove the unwanted tags and strip any unmatch tags too.
|
# Regex pattern to remove the text in the tags we don't want
|
||||||
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
|
pattern = re.compile('<movie>[^>]+.</movie>|<music>[^>]+.</music>', re.IGNORECASE|re.DOTALL)
|
||||||
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
|
elif metadata['media_type'] == 'artist' or metadata['media_type'] == 'track':
|
||||||
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
|
# Regex pattern to remove the text in the tags we don't want
|
||||||
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
|
pattern = re.compile('<tv>[^>]+.</tv>|<movie>[^>]+.</movie>', re.IGNORECASE|re.DOTALL)
|
||||||
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
|
else:
|
||||||
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
|
pattern = None
|
||||||
elif session['media_type'] == 'movie':
|
|
||||||
# Regex pattern to remove the text in the tags we don't want
|
|
||||||
pattern = re.compile('<tv>[^>]+.</tv>|<music>[^>]+.</music>', re.IGNORECASE)
|
|
||||||
|
|
||||||
# Remove the unwanted tags and strip any unmatch tags too.
|
|
||||||
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
|
|
||||||
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
|
|
||||||
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
|
|
||||||
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
|
|
||||||
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
|
|
||||||
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
|
|
||||||
elif session['media_type'] == 'track':
|
|
||||||
# Regex pattern to remove the text in the tags we don't want
|
|
||||||
pattern = re.compile('<tv>[^>]+.</tv>|<movie>[^>]+.</movie>', re.IGNORECASE)
|
|
||||||
|
|
||||||
|
if metadata['media_type'] == 'movie' \
|
||||||
|
or metadata['media_type'] == 'show' or metadata['media_type'] == 'episode' \
|
||||||
|
or metadata['media_type'] == 'artist' or metadata['media_type'] == 'track' \
|
||||||
|
and pattern:
|
||||||
# Remove the unwanted tags and strip any unmatch tags too.
|
# Remove the unwanted tags and strip any unmatch tags too.
|
||||||
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
|
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
|
||||||
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
|
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
|
||||||
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
|
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
|
||||||
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
|
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
|
||||||
|
on_pause_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT))
|
||||||
|
on_pause_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT))
|
||||||
|
on_resume_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT))
|
||||||
|
on_resume_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT))
|
||||||
|
on_buffer_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT))
|
||||||
|
on_buffer_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT))
|
||||||
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
|
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
|
||||||
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
|
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
|
||||||
|
on_created_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT))
|
||||||
|
on_created_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT))
|
||||||
else:
|
else:
|
||||||
on_start_subject = plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT
|
on_start_subject = plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT
|
||||||
on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT
|
on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT
|
||||||
on_stop_subject = plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT
|
on_stop_subject = plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT
|
||||||
on_stop_body = plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT
|
on_stop_body = plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT
|
||||||
|
on_pause_subject = plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT
|
||||||
|
on_pause_body = plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT
|
||||||
|
on_resume_subject = plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT
|
||||||
|
on_resume_body = plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT
|
||||||
|
on_buffer_subject = plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT
|
||||||
|
on_buffer_body = plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT
|
||||||
on_watched_subject = plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT
|
on_watched_subject = plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT
|
||||||
on_watched_body = plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT
|
on_watched_body = plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT
|
||||||
|
on_created_subject = plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT
|
||||||
|
on_created_body = plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT
|
||||||
|
|
||||||
# Create a title
|
# Create a title
|
||||||
if session['media_type'] == 'episode':
|
if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track':
|
||||||
full_title = '%s - %s' % (session['grandparent_title'],
|
full_title = '%s - %s' % (metadata['grandparent_title'],
|
||||||
session['title'])
|
metadata['title'])
|
||||||
elif session['media_type'] == 'track':
|
|
||||||
full_title = '%s - %s' % (session['grandparent_title'],
|
|
||||||
session['title'])
|
|
||||||
else:
|
else:
|
||||||
full_title = session['title']
|
full_title = metadata['title']
|
||||||
|
|
||||||
|
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
video_decision = ''
|
||||||
|
audio_decision = ''
|
||||||
|
transcode_decision = ''
|
||||||
|
stream_duration = 0
|
||||||
|
view_offset = 0
|
||||||
|
user = ''
|
||||||
|
platform = ''
|
||||||
|
player = ''
|
||||||
|
ip_address = 'N/A'
|
||||||
|
|
||||||
|
# Session values
|
||||||
|
if session:
|
||||||
# Generate a combined transcode decision value
|
# Generate a combined transcode decision value
|
||||||
if session['video_decision']:
|
video_decision = session['video_decision'].title()
|
||||||
if session['video_decision'] == 'transcode':
|
audio_decision = session['audio_decision'].title()
|
||||||
|
|
||||||
|
if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode':
|
||||||
transcode_decision = 'Transcode'
|
transcode_decision = 'Transcode'
|
||||||
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
|
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
|
||||||
transcode_decision = 'Direct Stream'
|
transcode_decision = 'Direct Stream'
|
||||||
else:
|
else:
|
||||||
transcode_decision = 'Direct Play'
|
transcode_decision = 'Direct Play'
|
||||||
else:
|
|
||||||
if session['audio_decision'] == 'transcode':
|
|
||||||
transcode_decision = 'Transcode'
|
|
||||||
else:
|
|
||||||
transcode_decision = 'Direct Play'
|
|
||||||
|
|
||||||
duration = helpers.convert_milliseconds_to_minutes(item_metadata['duration'])
|
if state != 'play':
|
||||||
|
if session['paused_counter']:
|
||||||
|
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) -
|
||||||
|
helpers.cast_to_float(session['paused_counter'])) / 60)
|
||||||
|
else:
|
||||||
|
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60)
|
||||||
|
|
||||||
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
|
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
|
||||||
|
user = session['friendly_name']
|
||||||
|
platform = session['platform']
|
||||||
|
player = session['player']
|
||||||
|
ip_address = session['ip_address'] if session['ip_address'] else 'N/A'
|
||||||
|
|
||||||
progress_percent = helpers.get_percent(view_offset, duration)
|
progress_percent = helpers.get_percent(view_offset, duration)
|
||||||
|
|
||||||
available_params = {'server_name': server_name,
|
available_params = {'server_name': server_name,
|
||||||
'user': session['friendly_name'],
|
'server_uptime': server_uptime,
|
||||||
'player': session['player'],
|
'user': user,
|
||||||
|
'platform': platform,
|
||||||
|
'player': player,
|
||||||
|
'ip_address': ip_address,
|
||||||
|
'media_type': metadata['media_type'],
|
||||||
'title': full_title,
|
'title': full_title,
|
||||||
'show_name': item_metadata['grandparent_title'],
|
'show_name': metadata['grandparent_title'],
|
||||||
'episode_name': item_metadata['title'],
|
'episode_name': metadata['title'],
|
||||||
'platform': session['platform'],
|
'artist_name': metadata['grandparent_title'],
|
||||||
'media_type': session['media_type'],
|
'album_name': metadata['parent_title'],
|
||||||
|
'track_name': metadata['title'],
|
||||||
|
'season_num': metadata['parent_index'].zfill(1),
|
||||||
|
'season_num00': metadata['parent_index'].zfill(2),
|
||||||
|
'episode_num': metadata['index'].zfill(1),
|
||||||
|
'episode_num00': metadata['index'].zfill(2),
|
||||||
|
'video_decision': video_decision,
|
||||||
|
'audio_decision': audio_decision,
|
||||||
'transcode_decision': transcode_decision,
|
'transcode_decision': transcode_decision,
|
||||||
'year': item_metadata['year'],
|
'year': metadata['year'],
|
||||||
'studio': item_metadata['studio'],
|
'studio': metadata['studio'],
|
||||||
'content_rating': item_metadata['content_rating'],
|
'content_rating': metadata['content_rating'],
|
||||||
'summary': item_metadata['summary'],
|
'directors': ', '.join(metadata['directors']),
|
||||||
'season_num': item_metadata['parent_index'],
|
'writers': ', '.join(metadata['writers']),
|
||||||
'episode_num': item_metadata['index'],
|
'actors': ', '.join(metadata['actors']),
|
||||||
'album_name': item_metadata['parent_title'],
|
'genres': ', '.join(metadata['genres']),
|
||||||
'rating': item_metadata['rating'],
|
'summary': metadata['summary'],
|
||||||
|
'tagline': metadata['tagline'],
|
||||||
|
'rating': metadata['rating'],
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
|
'stream_duration': stream_duration,
|
||||||
|
'remaining_duration': duration - view_offset,
|
||||||
'progress': view_offset,
|
'progress': view_offset,
|
||||||
'progress_percent': progress_percent
|
'progress_percent': progress_percent
|
||||||
}
|
}
|
||||||
@@ -273,14 +456,14 @@ def build_notify_text(session, state):
|
|||||||
|
|
||||||
if on_start_subject and on_start_body:
|
if on_start_subject and on_start_body:
|
||||||
try:
|
try:
|
||||||
subject_text = on_start_subject.format(**available_params)
|
subject_text = unicode(on_start_subject).format(**available_params)
|
||||||
except LookupError, e:
|
except LookupError, e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body_text = on_start_body.format(**available_params)
|
body_text = unicode(on_start_body).format(**available_params)
|
||||||
except LookupError, e:
|
except LookupError, e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
except:
|
except:
|
||||||
@@ -297,14 +480,86 @@ def build_notify_text(session, state):
|
|||||||
|
|
||||||
if on_stop_subject and on_stop_body:
|
if on_stop_subject and on_stop_body:
|
||||||
try:
|
try:
|
||||||
subject_text = on_stop_subject.format(**available_params)
|
subject_text = unicode(on_stop_subject).format(**available_params)
|
||||||
except LookupError, e:
|
except LookupError, e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body_text = on_stop_body.format(**available_params)
|
body_text = unicode(on_stop_body).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return [subject_text, body_text]
|
||||||
|
elif state == 'pause':
|
||||||
|
# Default body text
|
||||||
|
body_text = '%s (%s) has paused %s' % (session['friendly_name'],
|
||||||
|
session['player'],
|
||||||
|
full_title)
|
||||||
|
|
||||||
|
if on_pause_subject and on_pause_body:
|
||||||
|
try:
|
||||||
|
subject_text = unicode(on_pause_subject).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = unicode(on_pause_body).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return [subject_text, body_text]
|
||||||
|
elif state == 'resume':
|
||||||
|
# Default body text
|
||||||
|
body_text = '%s (%s) has resumed %s' % (session['friendly_name'],
|
||||||
|
session['player'],
|
||||||
|
full_title)
|
||||||
|
|
||||||
|
if on_resume_subject and on_resume_body:
|
||||||
|
try:
|
||||||
|
subject_text = unicode(on_resume_subject).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = unicode(on_resume_body).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return [subject_text, body_text]
|
||||||
|
elif state == 'buffer':
|
||||||
|
# Default body text
|
||||||
|
body_text = '%s (%s) is buffering %s' % (session['friendly_name'],
|
||||||
|
session['player'],
|
||||||
|
full_title)
|
||||||
|
|
||||||
|
if on_buffer_subject and on_buffer_body:
|
||||||
|
try:
|
||||||
|
subject_text = unicode(on_buffer_subject).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = unicode(on_buffer_body).format(**available_params)
|
||||||
except LookupError, e:
|
except LookupError, e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
except:
|
except:
|
||||||
@@ -321,14 +576,109 @@ def build_notify_text(session, state):
|
|||||||
|
|
||||||
if on_watched_subject and on_watched_body:
|
if on_watched_subject and on_watched_body:
|
||||||
try:
|
try:
|
||||||
subject_text = on_watched_subject.format(**available_params)
|
subject_text = unicode(on_watched_subject).format(**available_params)
|
||||||
except LookupError, e:
|
except LookupError, e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
except:
|
except:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body_text = on_watched_body.format(**available_params)
|
body_text = unicode(on_watched_body).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return [subject_text, body_text]
|
||||||
|
elif state == 'created':
|
||||||
|
# Default body text
|
||||||
|
body_text = '%s was recently added to Plex.' % full_title
|
||||||
|
|
||||||
|
if on_created_subject and on_created_body:
|
||||||
|
try:
|
||||||
|
subject_text = unicode(on_created_subject).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = unicode(on_created_body).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_server_notify_text(state=None):
|
||||||
|
# Get the server name
|
||||||
|
server_name = plexpy.CONFIG.PMS_NAME
|
||||||
|
|
||||||
|
# Get the server uptime
|
||||||
|
plex_tv = plextv.PlexTV()
|
||||||
|
server_times = plex_tv.get_server_times()
|
||||||
|
|
||||||
|
if server_times:
|
||||||
|
updated_at = server_times[0]['updated_at']
|
||||||
|
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
|
||||||
|
else:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to retrieve server uptime.")
|
||||||
|
server_uptime = 'N/A'
|
||||||
|
|
||||||
|
on_extdown_subject = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT
|
||||||
|
on_extdown_body = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT
|
||||||
|
on_intdown_subject = plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT
|
||||||
|
on_intdown_body = plexpy.CONFIG.NOTIFY_ON_INTDOWN_BODY_TEXT
|
||||||
|
|
||||||
|
available_params = {'server_name': server_name,
|
||||||
|
'server_uptime': server_uptime}
|
||||||
|
|
||||||
|
# Default text
|
||||||
|
subject_text = 'PlexPy (%s)' % server_name
|
||||||
|
|
||||||
|
if state == 'extdown':
|
||||||
|
# Default body text
|
||||||
|
body_text = 'The Plex Media Server remote access is down.'
|
||||||
|
|
||||||
|
if on_extdown_subject and on_extdown_body:
|
||||||
|
try:
|
||||||
|
subject_text = unicode(on_extdown_subject).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = unicode(on_extdown_body).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
|
||||||
|
|
||||||
|
return [subject_text, body_text]
|
||||||
|
else:
|
||||||
|
return [subject_text, body_text]
|
||||||
|
elif state == 'intdown':
|
||||||
|
# Default body text
|
||||||
|
body_text = 'The Plex Media Server is down.'
|
||||||
|
|
||||||
|
if on_intdown_subject and on_intdown_body:
|
||||||
|
try:
|
||||||
|
subject_text = unicode(on_intdown_subject).format(**available_params)
|
||||||
|
except LookupError, e:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
|
||||||
|
except:
|
||||||
|
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
body_text = unicode(on_intdown_body).format(**available_params)
|
||||||
except LookupError, e:
|
except LookupError, e:
|
||||||
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -49,7 +49,10 @@ AGENT_IDS = {"Growl": 0,
|
|||||||
"Pushover": 7,
|
"Pushover": 7,
|
||||||
"OSX Notify": 8,
|
"OSX Notify": 8,
|
||||||
"Boxcar2": 9,
|
"Boxcar2": 9,
|
||||||
"Email": 10}
|
"Email": 10,
|
||||||
|
"Twitter": 11,
|
||||||
|
"IFTTT": 12,
|
||||||
|
"Telegram": 13}
|
||||||
|
|
||||||
def available_notification_agents():
|
def available_notification_agents():
|
||||||
agents = [{'name': 'Growl',
|
agents = [{'name': 'Growl',
|
||||||
@@ -59,7 +62,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.GROWL_ENABLED),
|
'state': checked(plexpy.CONFIG.GROWL_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.GROWL_ON_PLAY,
|
'on_play': plexpy.CONFIG.GROWL_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.GROWL_ON_STOP,
|
'on_stop': plexpy.CONFIG.GROWL_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.GROWL_ON_WATCHED
|
'on_pause': plexpy.CONFIG.GROWL_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.GROWL_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.GROWL_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.GROWL_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.GROWL_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.GROWL_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.GROWL_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'Prowl',
|
{'name': 'Prowl',
|
||||||
'id': AGENT_IDS['Prowl'],
|
'id': AGENT_IDS['Prowl'],
|
||||||
@@ -68,7 +77,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.PROWL_ENABLED),
|
'state': checked(plexpy.CONFIG.PROWL_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.PROWL_ON_PLAY,
|
'on_play': plexpy.CONFIG.PROWL_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.PROWL_ON_STOP,
|
'on_stop': plexpy.CONFIG.PROWL_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.PROWL_ON_WATCHED
|
'on_pause': plexpy.CONFIG.PROWL_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.PROWL_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.PROWL_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.PROWL_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.PROWL_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.PROWL_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.PROWL_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'XBMC',
|
{'name': 'XBMC',
|
||||||
'id': AGENT_IDS['XBMC'],
|
'id': AGENT_IDS['XBMC'],
|
||||||
@@ -77,7 +92,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.XBMC_ENABLED),
|
'state': checked(plexpy.CONFIG.XBMC_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.XBMC_ON_PLAY,
|
'on_play': plexpy.CONFIG.XBMC_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.XBMC_ON_STOP,
|
'on_stop': plexpy.CONFIG.XBMC_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.XBMC_ON_WATCHED
|
'on_pause': plexpy.CONFIG.XBMC_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.XBMC_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.XBMC_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.XBMC_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.XBMC_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.XBMC_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.XBMC_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'Plex',
|
{'name': 'Plex',
|
||||||
'id': AGENT_IDS['Plex'],
|
'id': AGENT_IDS['Plex'],
|
||||||
@@ -86,7 +107,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.PLEX_ENABLED),
|
'state': checked(plexpy.CONFIG.PLEX_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.PLEX_ON_PLAY,
|
'on_play': plexpy.CONFIG.PLEX_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.PLEX_ON_STOP,
|
'on_stop': plexpy.CONFIG.PLEX_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.PLEX_ON_WATCHED
|
'on_pause': plexpy.CONFIG.PLEX_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.PLEX_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.PLEX_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.PLEX_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.PLEX_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.PLEX_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.PLEX_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'NotifyMyAndroid',
|
{'name': 'NotifyMyAndroid',
|
||||||
'id': AGENT_IDS['NMA'],
|
'id': AGENT_IDS['NMA'],
|
||||||
@@ -95,7 +122,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.NMA_ENABLED),
|
'state': checked(plexpy.CONFIG.NMA_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.NMA_ON_PLAY,
|
'on_play': plexpy.CONFIG.NMA_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.NMA_ON_STOP,
|
'on_stop': plexpy.CONFIG.NMA_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.NMA_ON_WATCHED
|
'on_pause': plexpy.CONFIG.NMA_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.NMA_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.NMA_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.NMA_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.NMA_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.NMA_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.NMA_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'Pushalot',
|
{'name': 'Pushalot',
|
||||||
'id': AGENT_IDS['Pushalot'],
|
'id': AGENT_IDS['Pushalot'],
|
||||||
@@ -104,7 +137,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.PUSHALOT_ENABLED),
|
'state': checked(plexpy.CONFIG.PUSHALOT_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.PUSHALOT_ON_PLAY,
|
'on_play': plexpy.CONFIG.PUSHALOT_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.PUSHALOT_ON_STOP,
|
'on_stop': plexpy.CONFIG.PUSHALOT_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED
|
'on_pause': plexpy.CONFIG.PUSHALOT_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.PUSHALOT_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.PUSHALOT_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.PUSHALOT_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.PUSHALOT_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.PUSHALOT_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'Pushbullet',
|
{'name': 'Pushbullet',
|
||||||
'id': AGENT_IDS['Pushbullet'],
|
'id': AGENT_IDS['Pushbullet'],
|
||||||
@@ -113,7 +152,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.PUSHBULLET_ENABLED),
|
'state': checked(plexpy.CONFIG.PUSHBULLET_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.PUSHBULLET_ON_PLAY,
|
'on_play': plexpy.CONFIG.PUSHBULLET_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.PUSHBULLET_ON_STOP,
|
'on_stop': plexpy.CONFIG.PUSHBULLET_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED
|
'on_pause': plexpy.CONFIG.PUSHBULLET_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.PUSHBULLET_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.PUSHBULLET_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.PUSHBULLET_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.PUSHBULLET_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.PUSHBULLET_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'Pushover',
|
{'name': 'Pushover',
|
||||||
'id': AGENT_IDS['Pushover'],
|
'id': AGENT_IDS['Pushover'],
|
||||||
@@ -122,7 +167,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.PUSHOVER_ENABLED),
|
'state': checked(plexpy.CONFIG.PUSHOVER_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.PUSHOVER_ON_PLAY,
|
'on_play': plexpy.CONFIG.PUSHOVER_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.PUSHOVER_ON_STOP,
|
'on_stop': plexpy.CONFIG.PUSHOVER_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.PUSHOVER_ON_WATCHED
|
'on_pause': plexpy.CONFIG.PUSHOVER_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.PUSHOVER_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.PUSHOVER_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.PUSHOVER_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.PUSHOVER_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.PUSHOVER_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.PUSHOVER_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'Boxcar2',
|
{'name': 'Boxcar2',
|
||||||
'id': AGENT_IDS['Boxcar2'],
|
'id': AGENT_IDS['Boxcar2'],
|
||||||
@@ -131,7 +182,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.BOXCAR_ENABLED),
|
'state': checked(plexpy.CONFIG.BOXCAR_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.BOXCAR_ON_PLAY,
|
'on_play': plexpy.CONFIG.BOXCAR_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.BOXCAR_ON_STOP,
|
'on_stop': plexpy.CONFIG.BOXCAR_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.BOXCAR_ON_WATCHED
|
'on_pause': plexpy.CONFIG.BOXCAR_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.BOXCAR_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.BOXCAR_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.BOXCAR_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.BOXCAR_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.BOXCAR_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.BOXCAR_ON_INTDOWN
|
||||||
},
|
},
|
||||||
{'name': 'E-mail',
|
{'name': 'E-mail',
|
||||||
'id': AGENT_IDS['Email'],
|
'id': AGENT_IDS['Email'],
|
||||||
@@ -140,7 +197,58 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.EMAIL_ENABLED),
|
'state': checked(plexpy.CONFIG.EMAIL_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.EMAIL_ON_PLAY,
|
'on_play': plexpy.CONFIG.EMAIL_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.EMAIL_ON_STOP,
|
'on_stop': plexpy.CONFIG.EMAIL_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED
|
'on_pause': plexpy.CONFIG.EMAIL_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.EMAIL_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.EMAIL_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.EMAIL_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.EMAIL_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.EMAIL_ON_INTDOWN
|
||||||
|
},
|
||||||
|
{'name': 'Twitter',
|
||||||
|
'id': AGENT_IDS['Twitter'],
|
||||||
|
'config_prefix': 'twitter',
|
||||||
|
'has_config': True,
|
||||||
|
'state': checked(plexpy.CONFIG.TWITTER_ENABLED),
|
||||||
|
'on_play': plexpy.CONFIG.TWITTER_ON_PLAY,
|
||||||
|
'on_stop': plexpy.CONFIG.TWITTER_ON_STOP,
|
||||||
|
'on_pause': plexpy.CONFIG.TWITTER_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.TWITTER_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.TWITTER_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.TWITTER_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.TWITTER_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.TWITTER_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.TWITTER_ON_INTDOWN
|
||||||
|
},
|
||||||
|
{'name': 'IFTTT',
|
||||||
|
'id': AGENT_IDS['IFTTT'],
|
||||||
|
'config_prefix': 'ifttt',
|
||||||
|
'has_config': True,
|
||||||
|
'state': checked(plexpy.CONFIG.IFTTT_ENABLED),
|
||||||
|
'on_play': plexpy.CONFIG.IFTTT_ON_PLAY,
|
||||||
|
'on_stop': plexpy.CONFIG.IFTTT_ON_STOP,
|
||||||
|
'on_pause': plexpy.CONFIG.IFTTT_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.IFTTT_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.IFTTT_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.IFTTT_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.IFTTT_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.IFTTT_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.IFTTT_ON_INTDOWN
|
||||||
|
},
|
||||||
|
{'name': 'Telegram',
|
||||||
|
'id': AGENT_IDS['Telegram'],
|
||||||
|
'config_prefix': 'telegram',
|
||||||
|
'has_config': True,
|
||||||
|
'state': checked(plexpy.CONFIG.TELEGRAM_ENABLED),
|
||||||
|
'on_play': plexpy.CONFIG.TELEGRAM_ON_PLAY,
|
||||||
|
'on_stop': plexpy.CONFIG.TELEGRAM_ON_STOP,
|
||||||
|
'on_pause': plexpy.CONFIG.TELEGRAM_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.TELEGRAM_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.TELEGRAM_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.TELEGRAM_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.TELEGRAM_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.TELEGRAM_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.TELEGRAM_ON_INTDOWN
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -154,7 +262,13 @@ def available_notification_agents():
|
|||||||
'state': checked(plexpy.CONFIG.OSX_NOTIFY_ENABLED),
|
'state': checked(plexpy.CONFIG.OSX_NOTIFY_ENABLED),
|
||||||
'on_play': plexpy.CONFIG.OSX_NOTIFY_ON_PLAY,
|
'on_play': plexpy.CONFIG.OSX_NOTIFY_ON_PLAY,
|
||||||
'on_stop': plexpy.CONFIG.OSX_NOTIFY_ON_STOP,
|
'on_stop': plexpy.CONFIG.OSX_NOTIFY_ON_STOP,
|
||||||
'on_watched': plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED
|
'on_pause': plexpy.CONFIG.OSX_NOTIFY_ON_PAUSE,
|
||||||
|
'on_resume': plexpy.CONFIG.OSX_NOTIFY_ON_RESUME,
|
||||||
|
'on_buffer': plexpy.CONFIG.OSX_NOTIFY_ON_BUFFER,
|
||||||
|
'on_watched': plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED,
|
||||||
|
'on_created': plexpy.CONFIG.OSX_NOTIFY_ON_CREATED,
|
||||||
|
'on_extdown': plexpy.CONFIG.OSX_NOTIFY_ON_EXTDOWN,
|
||||||
|
'on_intdown': plexpy.CONFIG.OSX_NOTIFY_ON_INTDOWN
|
||||||
})
|
})
|
||||||
|
|
||||||
return agents
|
return agents
|
||||||
@@ -196,6 +310,15 @@ def get_notification_agent_config(config_id):
|
|||||||
elif config_id == 10:
|
elif config_id == 10:
|
||||||
email = Email()
|
email = Email()
|
||||||
return email.return_config_options()
|
return email.return_config_options()
|
||||||
|
elif config_id == 11:
|
||||||
|
tweet = TwitterNotifier()
|
||||||
|
return tweet.return_config_options()
|
||||||
|
elif config_id == 12:
|
||||||
|
iftttClient = IFTTT()
|
||||||
|
return iftttClient.return_config_options()
|
||||||
|
elif config_id == 13:
|
||||||
|
telegramClient = TELEGRAM()
|
||||||
|
return telegramClient.return_config_options()
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
@@ -238,6 +361,15 @@ def send_notification(config_id, subject, body):
|
|||||||
elif config_id == 10:
|
elif config_id == 10:
|
||||||
email = Email()
|
email = Email()
|
||||||
email.notify(subject=subject, message=body)
|
email.notify(subject=subject, message=body)
|
||||||
|
elif config_id == 11:
|
||||||
|
tweet = TwitterNotifier()
|
||||||
|
tweet.notify(subject=subject, message=body)
|
||||||
|
elif config_id == 12:
|
||||||
|
iftttClient = IFTTT()
|
||||||
|
iftttClient.notify(subject=subject, message=body)
|
||||||
|
elif config_id == 13:
|
||||||
|
telegramClient = TELEGRAM()
|
||||||
|
telegramClient.notify(message=body, event=subject)
|
||||||
else:
|
else:
|
||||||
logger.debug(u"PlexPy Notifier :: Unknown agent id received.")
|
logger.debug(u"PlexPy Notifier :: Unknown agent id received.")
|
||||||
else:
|
else:
|
||||||
@@ -333,16 +465,16 @@ class GROWL(object):
|
|||||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'Host',
|
config_option = [{'label': 'Growl Host',
|
||||||
'value': self.host,
|
'value': self.host,
|
||||||
'name': 'growl_host',
|
'name': 'growl_host',
|
||||||
'description': 'Set the hostname.',
|
'description': 'Your Growl hostname.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Password',
|
{'label': 'Growl Password',
|
||||||
'value': self.password,
|
'value': self.password,
|
||||||
'name': 'growl_password',
|
'name': 'growl_password',
|
||||||
'description': 'Set the password.',
|
'description': 'Your Growl password.',
|
||||||
'input_type': 'password'
|
'input_type': 'password'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -373,7 +505,7 @@ class PROWL(object):
|
|||||||
|
|
||||||
data = {'apikey': plexpy.CONFIG.PROWL_KEYS,
|
data = {'apikey': plexpy.CONFIG.PROWL_KEYS,
|
||||||
'application': 'PlexPy',
|
'application': 'PlexPy',
|
||||||
'event': event,
|
'event': event.encode("utf-8"),
|
||||||
'description': message.encode("utf-8"),
|
'description': message.encode("utf-8"),
|
||||||
'priority': plexpy.CONFIG.PROWL_PRIORITY}
|
'priority': plexpy.CONFIG.PROWL_PRIORITY}
|
||||||
|
|
||||||
@@ -406,17 +538,18 @@ class PROWL(object):
|
|||||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'API Key',
|
config_option = [{'label': 'Prowl API Key',
|
||||||
'value': self.keys,
|
'value': self.keys,
|
||||||
'name': 'prowl_keys',
|
'name': 'prowl_keys',
|
||||||
'description': 'Set the API key.',
|
'description': 'Your Prowl API key.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Priority (-2,-1,0,1 or 2)',
|
{'label': 'Priority',
|
||||||
'value': self.priority,
|
'value': self.priority,
|
||||||
'name': 'prowl_priority',
|
'name': 'prowl_priority',
|
||||||
'description': 'Set the priority.',
|
'description': 'Set the priority.',
|
||||||
'input_type': 'number'
|
'input_type': 'select',
|
||||||
|
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -490,19 +623,19 @@ class XBMC(object):
|
|||||||
config_option = [{'label': 'XBMC Host:Port',
|
config_option = [{'label': 'XBMC Host:Port',
|
||||||
'value': self.hosts,
|
'value': self.hosts,
|
||||||
'name': 'xbmc_host',
|
'name': 'xbmc_host',
|
||||||
'description': 'e.g. http://localhost:8080. Separate hosts with commas.',
|
'description': 'Host running XBMC (e.g. http://localhost:8080). Separate multiple hosts with commas.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Username',
|
{'label': 'XBMC Username',
|
||||||
'value': self.username,
|
'value': self.username,
|
||||||
'name': 'xbmc_username',
|
'name': 'xbmc_username',
|
||||||
'description': 'Set the Username.',
|
'description': 'Your XBMC username.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Password',
|
{'label': 'XBMC Password',
|
||||||
'value': self.password,
|
'value': self.password,
|
||||||
'name': 'xbmc_password',
|
'name': 'xbmc_password',
|
||||||
'description': 'Set the Password.',
|
'description': 'Your XMBC password.',
|
||||||
'input_type': 'password'
|
'input_type': 'password'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -637,14 +770,15 @@ class NMA(object):
|
|||||||
config_option = [{'label': 'NotifyMyAndroid API Key',
|
config_option = [{'label': 'NotifyMyAndroid API Key',
|
||||||
'value': plexpy.CONFIG.NMA_APIKEY,
|
'value': plexpy.CONFIG.NMA_APIKEY,
|
||||||
'name': 'nma_apikey',
|
'name': 'nma_apikey',
|
||||||
'description': 'Separate multiple api keys with commas.',
|
'description': 'Your NotifyMyAndroid API key. Separate multiple api keys with commas.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Priority',
|
{'label': 'Priority',
|
||||||
'value': plexpy.CONFIG.NMA_PRIORITY,
|
'value': plexpy.CONFIG.NMA_PRIORITY,
|
||||||
'name': 'nma_priority',
|
'name': 'nma_priority',
|
||||||
'description': 'Priority (-2,-1,0,1 or 2).',
|
'description': 'Set the priority.',
|
||||||
'input_type': 'number'
|
'input_type': 'select',
|
||||||
|
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -709,7 +843,7 @@ class PUSHBULLET(object):
|
|||||||
self.notify('Main Screen Activate', 'Test Message')
|
self.notify('Main Screen Activate', 'Test Message')
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'API Key',
|
config_option = [{'label': 'Pushbullet API Key',
|
||||||
'value': self.apikey,
|
'value': self.apikey,
|
||||||
'name': 'pushbullet_apikey',
|
'name': 'pushbullet_apikey',
|
||||||
'description': 'Your Pushbullet API key.',
|
'description': 'Your Pushbullet API key.',
|
||||||
@@ -745,9 +879,9 @@ class PUSHALOT(object):
|
|||||||
|
|
||||||
pushalot_authorizationtoken = plexpy.CONFIG.PUSHALOT_APIKEY
|
pushalot_authorizationtoken = plexpy.CONFIG.PUSHALOT_APIKEY
|
||||||
|
|
||||||
logger.debug(u"Pushalot event: " + event)
|
#logger.debug(u"Pushalot event: " + event)
|
||||||
logger.debug(u"Pushalot message: " + message)
|
#logger.debug(u"Pushalot message: " + message)
|
||||||
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
|
#logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
|
||||||
|
|
||||||
http_handler = HTTPSConnection("pushalot.com")
|
http_handler = HTTPSConnection("pushalot.com")
|
||||||
|
|
||||||
@@ -762,9 +896,9 @@ class PUSHALOT(object):
|
|||||||
response = http_handler.getresponse()
|
response = http_handler.getresponse()
|
||||||
request_status = response.status
|
request_status = response.status
|
||||||
|
|
||||||
logger.debug(u"Pushalot response status: %r" % request_status)
|
#logger.debug(u"Pushalot response status: %r" % request_status)
|
||||||
logger.debug(u"Pushalot response headers: %r" % response.getheaders())
|
#logger.debug(u"Pushalot response headers: %r" % response.getheaders())
|
||||||
logger.debug(u"Pushalot response body: %r" % response.read())
|
#logger.debug(u"Pushalot response body: %r" % response.read())
|
||||||
|
|
||||||
if request_status == 200:
|
if request_status == 200:
|
||||||
logger.info(u"Pushalot notifications sent.")
|
logger.info(u"Pushalot notifications sent.")
|
||||||
@@ -777,7 +911,7 @@ class PUSHALOT(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'API Key',
|
config_option = [{'label': 'Pushalot API Key',
|
||||||
'value': plexpy.CONFIG.PUSHALOT_APIKEY,
|
'value': plexpy.CONFIG.PUSHALOT_APIKEY,
|
||||||
'name': 'pushalot_apikey',
|
'name': 'pushalot_apikey',
|
||||||
'description': 'Your Pushalot API key.',
|
'description': 'Your Pushalot API key.',
|
||||||
@@ -793,6 +927,7 @@ class PUSHOVER(object):
|
|||||||
self.enabled = plexpy.CONFIG.PUSHOVER_ENABLED
|
self.enabled = plexpy.CONFIG.PUSHOVER_ENABLED
|
||||||
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
|
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
|
||||||
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
|
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
|
||||||
|
self.sound = plexpy.CONFIG.PUSHOVER_SOUND
|
||||||
self.on_play = plexpy.CONFIG.PUSHOVER_ON_PLAY
|
self.on_play = plexpy.CONFIG.PUSHOVER_ON_PLAY
|
||||||
self.on_stop = plexpy.CONFIG.PUSHOVER_ON_STOP
|
self.on_stop = plexpy.CONFIG.PUSHOVER_ON_STOP
|
||||||
self.on_watched = plexpy.CONFIG.PUSHOVER_ON_WATCHED
|
self.on_watched = plexpy.CONFIG.PUSHOVER_ON_WATCHED
|
||||||
@@ -813,8 +948,9 @@ class PUSHOVER(object):
|
|||||||
|
|
||||||
data = {'token': self.application_token,
|
data = {'token': self.application_token,
|
||||||
'user': plexpy.CONFIG.PUSHOVER_KEYS,
|
'user': plexpy.CONFIG.PUSHOVER_KEYS,
|
||||||
'title': event,
|
'title': event.encode("utf-8"),
|
||||||
'message': message.encode("utf-8"),
|
'message': message.encode("utf-8"),
|
||||||
|
'sound': plexpy.CONFIG.PUSHOVER_SOUND,
|
||||||
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
|
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
|
||||||
|
|
||||||
http_handler.request("POST",
|
http_handler.request("POST",
|
||||||
@@ -841,30 +977,57 @@ class PUSHOVER(object):
|
|||||||
#For uniformity reasons not removed
|
#For uniformity reasons not removed
|
||||||
return
|
return
|
||||||
|
|
||||||
def test(self, keys, priority):
|
def test(self, keys, priority, sound):
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self.keys = keys
|
self.keys = keys
|
||||||
self.priority = priority
|
self.priority = priority
|
||||||
|
self.sound = sound
|
||||||
|
|
||||||
self.notify('Main Screen Activate', 'Test Message')
|
self.notify('Main Screen Activate', 'Test Message')
|
||||||
|
|
||||||
|
def get_sounds(self):
|
||||||
|
http_handler = HTTPSConnection("api.pushover.net")
|
||||||
|
http_handler.request("GET", "/1/sounds.json?token=" + self.application_token)
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
|
||||||
|
if request_status == 200:
|
||||||
|
data = json.loads(response.read())
|
||||||
|
sounds = data.get('sounds', {})
|
||||||
|
sounds.update({'': ''})
|
||||||
|
return sounds
|
||||||
|
elif request_status >= 400 and request_status < 500:
|
||||||
|
logger.info(u"Unable to retrieve Pushover notification sounds list: %s" % response.reason)
|
||||||
|
return {'': ''}
|
||||||
|
else:
|
||||||
|
logger.info(u"Unable to retrieve Pushover notification sounds list.")
|
||||||
|
return {'': ''}
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'API Key',
|
config_option = [{'label': 'Pushover User or Group Key',
|
||||||
'value': self.keys,
|
'value': self.keys,
|
||||||
'name': 'pushover_keys',
|
'name': 'pushover_keys',
|
||||||
'description': 'Your Pushover API key.',
|
'description': 'Your Pushover user or group key.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'Priority',
|
{'label': 'Priority',
|
||||||
'value': self.priority,
|
'value': self.priority,
|
||||||
'name': 'pushover_priority',
|
'name': 'pushover_priority',
|
||||||
'description': 'Priority (-1,0, or 1).',
|
'description': 'Set the priority.',
|
||||||
'input_type': 'number'
|
'input_type': 'select',
|
||||||
|
'select_options': {-2: -2, -1: -1, 0: 0, 1: 1, 2: 2}
|
||||||
},
|
},
|
||||||
{'label': 'API Token',
|
{'label': 'Sound',
|
||||||
|
'value': self.sound,
|
||||||
|
'name': 'pushover_sound',
|
||||||
|
'description': 'Set the notification sound. Leave blank for the default sound.',
|
||||||
|
'input_type': 'select',
|
||||||
|
'select_options': self.get_sounds()
|
||||||
|
},
|
||||||
|
{'label': 'Pushover API Token',
|
||||||
'value': plexpy.CONFIG.PUSHOVER_APITOKEN,
|
'value': plexpy.CONFIG.PUSHOVER_APITOKEN,
|
||||||
'name': 'pushover_apitoken',
|
'name': 'pushover_apitoken',
|
||||||
'description': 'Leave blank to use PlexPy default.',
|
'description': 'Your Pushover API token. Leave blank to use PlexPy default.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -879,19 +1042,17 @@ class TwitterNotifier(object):
|
|||||||
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
|
SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.consumer_key = "oYKnp2ddX5gbARjqX8ZAAg"
|
self.consumer_key = "2LdJKXHDUwJtjYBsdwJisIOsh"
|
||||||
self.consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk"
|
self.consumer_secret = "QWbUcZzAIiL4zbDCIhy2EdUkV8yEEav3qMdo5y3FugxCFelWrA"
|
||||||
|
|
||||||
def notify_snatch(self, title):
|
def notify(self, subject, message):
|
||||||
if plexpy.CONFIG.TWITTER_ONSNATCH:
|
if not subject or not message:
|
||||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
return
|
||||||
|
else:
|
||||||
def notify_download(self, title):
|
self._send_tweet(subject + ': ' + message)
|
||||||
if plexpy.CONFIG.TWITTER_ENABLED:
|
|
||||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
|
||||||
|
|
||||||
def test_notify(self):
|
def test_notify(self):
|
||||||
return self._notifyTwitter("This is a test notification from PlexPy at " + helpers.now(), force=True)
|
return self._send_tweet("This is a test notification from PlexPy at " + helpers.now())
|
||||||
|
|
||||||
def _get_authorization(self):
|
def _get_authorization(self):
|
||||||
|
|
||||||
@@ -925,7 +1086,7 @@ class TwitterNotifier(object):
|
|||||||
logger.info('Generating and signing request for an access token using key ' + key)
|
logger.info('Generating and signing request for an access token using key ' + key)
|
||||||
|
|
||||||
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret)
|
||||||
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
# logger.debug('oauth_consumer: ' + str(oauth_consumer))
|
||||||
oauth_client = oauth.Client(oauth_consumer, token)
|
oauth_client = oauth.Client(oauth_consumer, token)
|
||||||
logger.info('oauth_client: ' + str(oauth_client))
|
logger.info('oauth_client: ' + str(oauth_client))
|
||||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
|
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
|
||||||
@@ -946,7 +1107,6 @@ class TwitterNotifier(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def _send_tweet(self, message=None):
|
def _send_tweet(self, message=None):
|
||||||
|
|
||||||
username = self.consumer_key
|
username = self.consumer_key
|
||||||
password = self.consumer_secret
|
password = self.consumer_secret
|
||||||
access_token_key = plexpy.CONFIG.TWITTER_USERNAME
|
access_token_key = plexpy.CONFIG.TWITTER_USERNAME
|
||||||
@@ -964,13 +1124,36 @@ class TwitterNotifier(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notifyTwitter(self, message='', force=False):
|
def return_config_options(self):
|
||||||
prefix = plexpy.CONFIG.TWITTER_PREFIX
|
config_option = [{'label': 'Request Authorisation',
|
||||||
|
'value': 'Request Authorisation',
|
||||||
|
'name': 'twitterStep1',
|
||||||
|
'description': 'Step 1: Click Request button above. (Ensure you allow the browser pop-up).',
|
||||||
|
'input_type': 'button'
|
||||||
|
},
|
||||||
|
{'label': 'Authorisation Key',
|
||||||
|
'value': '',
|
||||||
|
'name': 'twitter_key',
|
||||||
|
'description': 'Step 2: Input the authorisation key you received from Step 1.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Verify Key',
|
||||||
|
'value': 'Verify Key',
|
||||||
|
'name': 'twitterStep2',
|
||||||
|
'description': 'Step 3: Verify the key.',
|
||||||
|
'input_type': 'button'
|
||||||
|
},
|
||||||
|
{'label': 'Test Twitter',
|
||||||
|
'value': 'Test Twitter',
|
||||||
|
'name': 'testTwitter',
|
||||||
|
'description': 'Test if Twitter notifications are working. See logs for troubleshooting.',
|
||||||
|
'input_type': 'button'
|
||||||
|
},
|
||||||
|
{'input_type': 'nosave'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
if not plexpy.CONFIG.TWITTER_ENABLED and not force:
|
return config_option
|
||||||
return False
|
|
||||||
|
|
||||||
return self._send_tweet(prefix + ": " + message)
|
|
||||||
|
|
||||||
class OSX_NOTIFY(object):
|
class OSX_NOTIFY(object):
|
||||||
|
|
||||||
@@ -1034,6 +1217,7 @@ class OSX_NOTIFY(object):
|
|||||||
|
|
||||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||||
notification_center.deliverNotification_(notification)
|
notification_center.deliverNotification_(notification)
|
||||||
|
logger.info(u"OSX Notify notifications sent.")
|
||||||
|
|
||||||
del pool
|
del pool
|
||||||
return True
|
return True
|
||||||
@@ -1062,6 +1246,7 @@ class BOXCAR(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.url = 'https://new.boxcar.io/api/notifications'
|
self.url = 'https://new.boxcar.io/api/notifications'
|
||||||
self.token = plexpy.CONFIG.BOXCAR_TOKEN
|
self.token = plexpy.CONFIG.BOXCAR_TOKEN
|
||||||
|
self.sound = plexpy.CONFIG.BOXCAR_SOUND
|
||||||
self.on_play = plexpy.CONFIG.BOXCAR_ON_PLAY
|
self.on_play = plexpy.CONFIG.BOXCAR_ON_PLAY
|
||||||
self.on_stop = plexpy.CONFIG.BOXCAR_ON_STOP
|
self.on_stop = plexpy.CONFIG.BOXCAR_ON_STOP
|
||||||
self.on_watched = plexpy.CONFIG.BOXCAR_ON_WATCHED
|
self.on_watched = plexpy.CONFIG.BOXCAR_ON_WATCHED
|
||||||
@@ -1075,7 +1260,7 @@ class BOXCAR(object):
|
|||||||
'user_credentials': plexpy.CONFIG.BOXCAR_TOKEN,
|
'user_credentials': plexpy.CONFIG.BOXCAR_TOKEN,
|
||||||
'notification[title]': title.encode('utf-8'),
|
'notification[title]': title.encode('utf-8'),
|
||||||
'notification[long_message]': message.encode('utf-8'),
|
'notification[long_message]': message.encode('utf-8'),
|
||||||
'notification[sound]': "done"
|
'notification[sound]': plexpy.CONFIG.BOXCAR_SOUND
|
||||||
})
|
})
|
||||||
|
|
||||||
req = urllib2.Request(self.url)
|
req = urllib2.Request(self.url)
|
||||||
@@ -1088,11 +1273,47 @@ class BOXCAR(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def return_config_options(self):
|
def return_config_options(self):
|
||||||
config_option = [{'label': 'Access Token',
|
config_option = [{'label': 'Boxcar Access Token',
|
||||||
'value': plexpy.CONFIG.BOXCAR_TOKEN,
|
'value': plexpy.CONFIG.BOXCAR_TOKEN,
|
||||||
'name': 'boxcar_token',
|
'name': 'boxcar_token',
|
||||||
'description': 'Your Boxcar access token.',
|
'description': 'Your Boxcar access token.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Sound',
|
||||||
|
'value': self.sound,
|
||||||
|
'name': 'boxcar_sound',
|
||||||
|
'description': 'Set the notification sound. Leave blank for the default sound.',
|
||||||
|
'input_type': 'select',
|
||||||
|
'select_options': {'': '',
|
||||||
|
'beep-crisp': 'Beep (Crisp)',
|
||||||
|
'beep-soft': 'Beep (Soft)',
|
||||||
|
'bell-modern': 'Bell (Modern)',
|
||||||
|
'bell-one-tone': 'Bell (One Tone)',
|
||||||
|
'bell-simple': 'Bell (Simple)',
|
||||||
|
'bell-triple': 'Bell (Triple)',
|
||||||
|
'bird-1': 'Bird (1)',
|
||||||
|
'bird-2': 'Bird (2)',
|
||||||
|
'boing': 'Boing',
|
||||||
|
'cash': 'Cash',
|
||||||
|
'clanging': 'Clanging',
|
||||||
|
'detonator-charge': 'Detonator Charge',
|
||||||
|
'digital-alarm': 'Digital Alarm',
|
||||||
|
'done': 'Done',
|
||||||
|
'echo': 'Echo',
|
||||||
|
'flourish': 'Flourish',
|
||||||
|
'harp': 'Harp',
|
||||||
|
'light': 'Light',
|
||||||
|
'magic-chime':'Magic Chime',
|
||||||
|
'magic-coin': 'Magic Coin',
|
||||||
|
'no-sound': 'No Sound',
|
||||||
|
'notifier-1': 'Notifier (1)',
|
||||||
|
'notifier-2': 'Notifier (2)',
|
||||||
|
'notifier-3': 'Notifier (3)',
|
||||||
|
'orchestral-long': 'Orchestral (Long)',
|
||||||
|
'orchestral-short': 'Orchestral (Short)',
|
||||||
|
'score': 'Score',
|
||||||
|
'success': 'Success',
|
||||||
|
'up': 'Up'}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1127,6 +1348,8 @@ class Email(object):
|
|||||||
|
|
||||||
mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string())
|
mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string())
|
||||||
mailserver.quit()
|
mailserver.quit()
|
||||||
|
|
||||||
|
logger.info(u"Email notifications sent.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
@@ -1143,7 +1366,7 @@ class Email(object):
|
|||||||
{'label': 'To',
|
{'label': 'To',
|
||||||
'value': plexpy.CONFIG.EMAIL_TO,
|
'value': plexpy.CONFIG.EMAIL_TO,
|
||||||
'name': 'email_to',
|
'name': 'email_to',
|
||||||
'description': 'Who should the recipeint be.',
|
'description': 'Who should the recipient be.',
|
||||||
'input_type': 'text'
|
'input_type': 'text'
|
||||||
},
|
},
|
||||||
{'label': 'SMTP Server',
|
{'label': 'SMTP Server',
|
||||||
@@ -1171,7 +1394,7 @@ class Email(object):
|
|||||||
'input_type': 'password'
|
'input_type': 'password'
|
||||||
},
|
},
|
||||||
{'label': 'TLS',
|
{'label': 'TLS',
|
||||||
'value': checked(plexpy.CONFIG.EMAIL_TLS),
|
'value': plexpy.CONFIG.EMAIL_TLS,
|
||||||
'name': 'email_tls',
|
'name': 'email_tls',
|
||||||
'description': 'Does the server use encryption.',
|
'description': 'Does the server use encryption.',
|
||||||
'input_type': 'checkbox'
|
'input_type': 'checkbox'
|
||||||
@@ -1179,3 +1402,133 @@ class Email(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
return config_option
|
return config_option
|
||||||
|
|
||||||
|
|
||||||
|
class IFTTT(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.apikey = plexpy.CONFIG.IFTTT_KEY
|
||||||
|
self.event = plexpy.CONFIG.IFTTT_EVENT
|
||||||
|
|
||||||
|
def notify(self, message, subject):
|
||||||
|
if not message or not subject:
|
||||||
|
return
|
||||||
|
|
||||||
|
http_handler = HTTPSConnection("maker.ifttt.com")
|
||||||
|
|
||||||
|
data = {'value1': subject.encode("utf-8"),
|
||||||
|
'value2': message.encode("utf-8")}
|
||||||
|
|
||||||
|
# logger.debug("Ifttt SENDING: %s" % json.dumps(data))
|
||||||
|
|
||||||
|
http_handler.request("POST",
|
||||||
|
"/trigger/%s/with/key/%s" % (self.event, self.apikey),
|
||||||
|
headers={'Content-type': "application/json"},
|
||||||
|
body=json.dumps(data))
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
# logger.debug(u"Ifttt response status: %r" % request_status)
|
||||||
|
# logger.debug(u"Ifttt response headers: %r" % response.getheaders())
|
||||||
|
# logger.debug(u"Ifttt response body: %r" % response.read())
|
||||||
|
|
||||||
|
if request_status == 200:
|
||||||
|
logger.info(u"Ifttt notifications sent.")
|
||||||
|
return True
|
||||||
|
elif request_status >= 400 and request_status < 500:
|
||||||
|
logger.info(u"Ifttt request failed: %s" % response.reason)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.info(u"Ifttt notification failed serverside.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
return self.notify('PlexPy', 'Test Message')
|
||||||
|
|
||||||
|
def return_config_options(self):
|
||||||
|
config_option = [{'label': 'Ifttt Maker Channel Key',
|
||||||
|
'value': self.apikey,
|
||||||
|
'name': 'ifttt_key',
|
||||||
|
'description': 'Your Ifttt key. You can get a key from <a href="https://ifttt.com/maker" target="_blank">here</a>.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Ifttt Event',
|
||||||
|
'value': self.event,
|
||||||
|
'name': 'ifttt_event',
|
||||||
|
'description': 'The Ifttt maker event to fire. The notification subject and body will be sent'
|
||||||
|
' as value1 and value2 respectively.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Test Event',
|
||||||
|
'value': 'Test Event',
|
||||||
|
'name': 'testIFTTT',
|
||||||
|
'description': 'Test if IFTTT notifications are working. See logs for troubleshooting.',
|
||||||
|
'input_type': 'button'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return config_option
|
||||||
|
|
||||||
|
class TELEGRAM(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
|
||||||
|
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
|
||||||
|
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
|
||||||
|
|
||||||
|
def conf(self, options):
|
||||||
|
return cherrypy.config['config'].get('Telegram', options)
|
||||||
|
|
||||||
|
def notify(self, message, event):
|
||||||
|
if not message or not event:
|
||||||
|
return
|
||||||
|
|
||||||
|
http_handler = HTTPSConnection("api.telegram.org")
|
||||||
|
|
||||||
|
data = {'chat_id': self.chat_id,
|
||||||
|
'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
|
||||||
|
|
||||||
|
http_handler.request("POST",
|
||||||
|
"/bot%s/%s" % (self.bot_token, "sendMessage"),
|
||||||
|
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||||
|
body=urlencode(data))
|
||||||
|
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
|
||||||
|
if request_status == 200:
|
||||||
|
logger.info(u"Telegram notifications sent.")
|
||||||
|
return True
|
||||||
|
elif request_status >= 400 and request_status < 500:
|
||||||
|
logger.info(u"Telegram request failed: %s" % response.reason)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.info(u"Telegram notification failed serverside.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def updateLibrary(self):
|
||||||
|
#For uniformity reasons not removed
|
||||||
|
return
|
||||||
|
|
||||||
|
def test(self, bot_token, chat_id):
|
||||||
|
self.enabled = True
|
||||||
|
self.bot_token = bot_token
|
||||||
|
self.chat_id = chat_id
|
||||||
|
|
||||||
|
self.notify('Main Screen Activate', 'Test Message')
|
||||||
|
|
||||||
|
def return_config_options(self):
|
||||||
|
config_option = [{'label': 'Telegram Bot Token',
|
||||||
|
'value': self.bot_token,
|
||||||
|
'name': 'telegram_bot_token',
|
||||||
|
'description': 'Your Telegram bot token. Contact <a href="http://telegram.me/BotFather" target="_blank">@BotFather</a> on Telegram to get one.',
|
||||||
|
'input_type': 'text'
|
||||||
|
},
|
||||||
|
{'label': 'Telegram Chat ID',
|
||||||
|
'value': self.chat_id,
|
||||||
|
'name': 'telegram_chat_id',
|
||||||
|
'description': 'Your Telegram Chat ID, Group ID, or channel username. Contact <a href="http://telegram.me/myidbot" target="_blank">@myidbot</a> on Telegram to get an ID.',
|
||||||
|
'input_type': 'text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return config_option
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
@@ -13,13 +16,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger, helpers, datafactory, http_handler, database
|
from plexpy import logger, helpers, users, http_handler, database
|
||||||
|
import xmltodict
|
||||||
|
import json
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import plexpy
|
import plexpy
|
||||||
|
|
||||||
|
|
||||||
def refresh_users():
|
def refresh_users():
|
||||||
logger.info("Requesting users list refresh...")
|
logger.info("Requesting users list refresh...")
|
||||||
result = PlexTV().get_full_users_list()
|
result = PlexTV().get_full_users_list()
|
||||||
@@ -27,9 +32,8 @@ def refresh_users():
|
|||||||
|
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
for item in result:
|
for item in result:
|
||||||
control_value_dict = {"username": item['username']}
|
control_value_dict = {"user_id": item['user_id']}
|
||||||
new_value_dict = {"user_id": item['user_id'],
|
new_value_dict = {"username": item['username'],
|
||||||
"username": item['username'],
|
|
||||||
"thumb": item['thumb'],
|
"thumb": item['thumb'],
|
||||||
"email": item['email'],
|
"email": item['email'],
|
||||||
"is_home_user": item['is_home_user'],
|
"is_home_user": item['is_home_user'],
|
||||||
@@ -37,12 +41,25 @@ def refresh_users():
|
|||||||
"is_restricted": item['is_restricted']
|
"is_restricted": item['is_restricted']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check if we've set a custom avatar if so don't overwrite it.
|
||||||
|
if item['user_id']:
|
||||||
|
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
|
||||||
|
'FROM users WHERE user_id = ?',
|
||||||
|
[item['user_id']])
|
||||||
|
if avatar_urls:
|
||||||
|
if not avatar_urls[0]['custom_avatar_url'] or \
|
||||||
|
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
|
||||||
|
new_value_dict['custom_avatar_url'] = item['thumb']
|
||||||
|
else:
|
||||||
|
new_value_dict['custom_avatar_url'] = item['thumb']
|
||||||
|
|
||||||
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||||
|
|
||||||
logger.info("Users list refreshed.")
|
logger.info("Users list refreshed.")
|
||||||
else:
|
else:
|
||||||
logger.warn("Unable to refresh users list.")
|
logger.warn("Unable to refresh users list.")
|
||||||
|
|
||||||
|
|
||||||
def get_real_pms_url():
|
def get_real_pms_url():
|
||||||
logger.info("Requesting URLs for server...")
|
logger.info("Requesting URLs for server...")
|
||||||
|
|
||||||
@@ -80,6 +97,7 @@ def get_real_pms_url():
|
|||||||
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
|
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
|
||||||
plexpy.CONFIG.write()
|
plexpy.CONFIG.write()
|
||||||
|
|
||||||
|
|
||||||
class PlexTV(object):
|
class PlexTV(object):
|
||||||
"""
|
"""
|
||||||
Plex.tv authentication
|
Plex.tv authentication
|
||||||
@@ -122,7 +140,7 @@ class PlexTV(object):
|
|||||||
if plextv_response:
|
if plextv_response:
|
||||||
xml_head = plextv_response.getElementsByTagName('user')
|
xml_head = plextv_response.getElementsByTagName('user')
|
||||||
if not xml_head:
|
if not xml_head:
|
||||||
logger.warn("Error parsing XML for Plex.tv token: %s" % e)
|
logger.warn("Error parsing XML for Plex.tv token")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
auth_token = xml_head[0].getAttribute('authenticationToken')
|
auth_token = xml_head[0].getAttribute('authenticationToken')
|
||||||
@@ -244,7 +262,7 @@ class PlexTV(object):
|
|||||||
|
|
||||||
def get_synced_items(self, machine_id=None, user_id=None):
|
def get_synced_items(self, machine_id=None, user_id=None):
|
||||||
sync_list = self.get_plextv_sync_lists(machine_id)
|
sync_list = self.get_plextv_sync_lists(machine_id)
|
||||||
data_factory = datafactory.DataFactory()
|
user_data = users.Users()
|
||||||
|
|
||||||
synced_items = []
|
synced_items = []
|
||||||
|
|
||||||
@@ -268,8 +286,8 @@ class PlexTV(object):
|
|||||||
for device in sync_device:
|
for device in sync_device:
|
||||||
device_user_id = helpers.get_xml_attr(device, 'userID')
|
device_user_id = helpers.get_xml_attr(device, 'userID')
|
||||||
try:
|
try:
|
||||||
device_username = data_factory.get_user_details(user_id=device_user_id)['username']
|
device_username = user_data.get_user_details(user_id=device_user_id)['username']
|
||||||
device_friendly_name = data_factory.get_user_details(user_id=device_user_id)['friendly_name']
|
device_friendly_name = user_data.get_user_details(user_id=device_user_id)['friendly_name']
|
||||||
except:
|
except:
|
||||||
device_username = ''
|
device_username = ''
|
||||||
device_friendly_name = ''
|
device_friendly_name = ''
|
||||||
@@ -391,3 +409,57 @@ class PlexTV(object):
|
|||||||
server_urls.append(server_details)
|
server_urls.append(server_details)
|
||||||
|
|
||||||
return server_urls
|
return server_urls
|
||||||
|
|
||||||
|
def discover(self):
|
||||||
|
""" Query plex for all servers online. Returns the ones you own in a selectize format """
|
||||||
|
result = self.get_plextv_resources(include_https=True, output_format='raw')
|
||||||
|
servers = xmltodict.parse(result, process_namespaces=True, attr_prefix='')
|
||||||
|
clean_servers = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if servers:
|
||||||
|
# Fix if its only one "device"
|
||||||
|
if int(servers['MediaContainer']['size']) == 1:
|
||||||
|
servers['MediaContainer']['Device'] = [servers['MediaContainer']['Device']]
|
||||||
|
|
||||||
|
for server in servers['MediaContainer']['Device']:
|
||||||
|
# Only grab servers online and own
|
||||||
|
if server.get('presence', None) == '1' and server.get('owned', None) == '1' and server.get('provides', None) == 'server':
|
||||||
|
# If someone only has one connection..
|
||||||
|
if isinstance(server['Connection'], dict):
|
||||||
|
server['Connection'] = [server['Connection']]
|
||||||
|
|
||||||
|
for s in server['Connection']:
|
||||||
|
# to avoid circular ref
|
||||||
|
d = {}
|
||||||
|
d.update(s)
|
||||||
|
d.update(server)
|
||||||
|
d['label'] = d['name']
|
||||||
|
d['value'] = d['address']
|
||||||
|
del d['Connection']
|
||||||
|
clean_servers.append(d)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn('Failed to get servers from plex %s' % e)
|
||||||
|
return clean_servers
|
||||||
|
|
||||||
|
return json.dumps(clean_servers, indent=4)
|
||||||
|
|
||||||
|
def get_server_times(self):
|
||||||
|
servers = self.get_plextv_server_list(output_format='xml')
|
||||||
|
server_times = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
xml_head = servers.getElementsByTagName('Server')
|
||||||
|
except:
|
||||||
|
logger.warn("Error parsing XML for Plex servers.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
for a in xml_head:
|
||||||
|
if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER:
|
||||||
|
server_times.append({"created_at": helpers.get_xml_attr(a, 'createdAt'),
|
||||||
|
"updated_at": helpers.get_xml_attr(a, 'updatedAt')
|
||||||
|
})
|
||||||
|
break
|
||||||
|
|
||||||
|
return server_times
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# This file is part of PlexPy.
|
# This file is part of PlexPy.
|
||||||
#
|
#
|
||||||
# PlexPy is free software: you can redistribute it and/or modify
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
from plexpy import logger, helpers, monitor, datafactory, plextv
|
from plexpy import logger, helpers, activity_pinger, activity_processor, users, plextv
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
@@ -55,6 +55,7 @@ def extract_plexwatch_xml(xml=None):
|
|||||||
parent_title = helpers.get_xml_attr(a, 'parentTitle')
|
parent_title = helpers.get_xml_attr(a, 'parentTitle')
|
||||||
studio = helpers.get_xml_attr(a, 'studio')
|
studio = helpers.get_xml_attr(a, 'studio')
|
||||||
title = helpers.get_xml_attr(a, 'title')
|
title = helpers.get_xml_attr(a, 'title')
|
||||||
|
tagline = helpers.get_xml_attr(a, 'tagline')
|
||||||
|
|
||||||
directors = []
|
directors = []
|
||||||
if a.getElementsByTagName('Director'):
|
if a.getElementsByTagName('Director'):
|
||||||
@@ -153,6 +154,7 @@ def extract_plexwatch_xml(xml=None):
|
|||||||
'grandparent_title': grandparent_title,
|
'grandparent_title': grandparent_title,
|
||||||
'parent_title': parent_title,
|
'parent_title': parent_title,
|
||||||
'title': title,
|
'title': title,
|
||||||
|
'tagline': tagline,
|
||||||
'guid': guid,
|
'guid': guid,
|
||||||
'media_index': media_index,
|
'media_index': media_index,
|
||||||
'originally_available_at': originally_available_at,
|
'originally_available_at': originally_available_at,
|
||||||
@@ -243,10 +245,13 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
|||||||
logger.debug(u"PlexPy Importer :: PlexWatch data import in progress...")
|
logger.debug(u"PlexPy Importer :: PlexWatch data import in progress...")
|
||||||
|
|
||||||
logger.debug(u"PlexPy Importer :: Disabling monitoring while import in progress.")
|
logger.debug(u"PlexPy Importer :: Disabling monitoring while import in progress.")
|
||||||
plexpy.schedule_job(monitor.check_active_sessions, 'Check for active sessions', hours=0, minutes=0, seconds=0)
|
plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
|
||||||
|
hours=0, minutes=0, seconds=0)
|
||||||
|
plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||||
|
hours=0, minutes=0, seconds=0)
|
||||||
|
|
||||||
monitor_processing = monitor.MonitorProcessing()
|
ap = activity_processor.ActivityProcessor()
|
||||||
data_factory = datafactory.DataFactory()
|
user_data = users.Users()
|
||||||
|
|
||||||
# Get the latest friends list so we can pull user id's
|
# Get the latest friends list so we can pull user id's
|
||||||
try:
|
try:
|
||||||
@@ -287,13 +292,18 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
|||||||
|
|
||||||
# If we get back None from our xml extractor skip over the record and log error.
|
# If we get back None from our xml extractor skip over the record and log error.
|
||||||
if not extracted_xml:
|
if not extracted_xml:
|
||||||
logger.error(u"PlexPy Importer :: Skipping line with ratingKey %s due to malformed xml."
|
logger.error(u"PlexPy Importer :: Skipping record with ratingKey %s due to malformed xml."
|
||||||
% str(row['rating_key']))
|
% str(row['rating_key']))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Skip line if we don't have a ratingKey to work with
|
||||||
|
if not row['rating_key']:
|
||||||
|
logger.error(u"PlexPy Importer :: Skipping record due to null ratingRey.")
|
||||||
|
continue
|
||||||
|
|
||||||
# If the user_id no longer exists in the friends list, pull it from the xml.
|
# If the user_id no longer exists in the friends list, pull it from the xml.
|
||||||
if data_factory.get_user_id(user=row['user']):
|
if user_data.get_user_id(user=row['user']):
|
||||||
user_id = data_factory.get_user_id(user=row['user'])
|
user_id = user_data.get_user_id(user=row['user'])
|
||||||
else:
|
else:
|
||||||
user_id = extracted_xml['user_id']
|
user_id = extracted_xml['user_id']
|
||||||
|
|
||||||
@@ -356,6 +366,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
|||||||
'last_viewed_at': extracted_xml['last_viewed_at'],
|
'last_viewed_at': extracted_xml['last_viewed_at'],
|
||||||
'content_rating': row['content_rating'],
|
'content_rating': row['content_rating'],
|
||||||
'summary': row['summary'],
|
'summary': row['summary'],
|
||||||
|
'tagline': extracted_xml['tagline'],
|
||||||
'rating': extracted_xml['rating'],
|
'rating': extracted_xml['rating'],
|
||||||
'duration': extracted_xml['duration'],
|
'duration': extracted_xml['duration'],
|
||||||
'guid': extracted_xml['guid'],
|
'guid': extracted_xml['guid'],
|
||||||
@@ -370,7 +381,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
|||||||
# On older versions of PMS, "clip" items were still classified as "movie" and had bad ratingKey values
|
# On older versions of PMS, "clip" items were still classified as "movie" and had bad ratingKey values
|
||||||
# Just make sure that the ratingKey is indeed an integer
|
# Just make sure that the ratingKey is indeed an integer
|
||||||
if session_history_metadata['rating_key'].isdigit():
|
if session_history_metadata['rating_key'].isdigit():
|
||||||
monitor_processing.write_session_history(session=session_history,
|
ap.write_session_history(session=session_history,
|
||||||
import_metadata=session_history_metadata,
|
import_metadata=session_history_metadata,
|
||||||
is_import=True,
|
is_import=True,
|
||||||
import_ignore_interval=import_ignore_interval)
|
import_ignore_interval=import_ignore_interval)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
548
plexpy/users.py
Normal file
548
plexpy/users.py
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PlexPy is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from plexpy import logger, datatables, common, database, helpers
|
||||||
|
|
||||||
|
|
||||||
|
class Users(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_user_list(self, kwargs=None):
|
||||||
|
data_tables = datatables.DataTables()
|
||||||
|
|
||||||
|
custom_where = ['users.deleted_user', 0]
|
||||||
|
|
||||||
|
columns = ['session_history.id',
|
||||||
|
'users.user_id as user_id',
|
||||||
|
'users.custom_avatar_url as user_thumb',
|
||||||
|
'(case when users.friendly_name is null then users.username else \
|
||||||
|
users.friendly_name end) as friendly_name',
|
||||||
|
'MAX(session_history.started) as last_seen',
|
||||||
|
'session_history.ip_address as ip_address',
|
||||||
|
'COUNT(session_history.id) as plays',
|
||||||
|
'session_history.platform as platform',
|
||||||
|
'session_history.player as player',
|
||||||
|
'session_history_metadata.full_title as last_watched',
|
||||||
|
'session_history_metadata.thumb',
|
||||||
|
'session_history_metadata.parent_thumb',
|
||||||
|
'session_history_metadata.grandparent_thumb',
|
||||||
|
'session_history_metadata.media_type',
|
||||||
|
'session_history.rating_key as rating_key',
|
||||||
|
'session_history_media_info.video_decision',
|
||||||
|
'users.username as user',
|
||||||
|
'users.do_notify as do_notify',
|
||||||
|
'users.keep_history as keep_history'
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
query = data_tables.ssp_query(table_name='users',
|
||||||
|
columns=columns,
|
||||||
|
custom_where=[custom_where],
|
||||||
|
group_by=['users.user_id'],
|
||||||
|
join_types=['LEFT OUTER JOIN',
|
||||||
|
'LEFT OUTER JOIN',
|
||||||
|
'LEFT OUTER JOIN'],
|
||||||
|
join_tables=['session_history',
|
||||||
|
'session_history_metadata',
|
||||||
|
'session_history_media_info'],
|
||||||
|
join_evals=[['session_history.user_id', 'users.user_id'],
|
||||||
|
['session_history.id', 'session_history_metadata.id'],
|
||||||
|
['session_history.id', 'session_history_media_info.id']],
|
||||||
|
kwargs=kwargs)
|
||||||
|
except:
|
||||||
|
logger.warn("Unable to execute database query.")
|
||||||
|
return {'recordsFiltered': 0,
|
||||||
|
'recordsTotal': 0,
|
||||||
|
'draw': 0,
|
||||||
|
'data': 'null',
|
||||||
|
'error': 'Unable to execute database query.'}
|
||||||
|
|
||||||
|
users = query['result']
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for item in users:
|
||||||
|
if item["media_type"] == 'episode' and item["parent_thumb"]:
|
||||||
|
thumb = item["parent_thumb"]
|
||||||
|
elif item["media_type"] == 'episode':
|
||||||
|
thumb = item["grandparent_thumb"]
|
||||||
|
else:
|
||||||
|
thumb = item["thumb"]
|
||||||
|
|
||||||
|
if not item['user_thumb'] or item['user_thumb'] == '':
|
||||||
|
user_thumb = common.DEFAULT_USER_THUMB
|
||||||
|
else:
|
||||||
|
user_thumb = item['user_thumb']
|
||||||
|
|
||||||
|
# Rename Mystery platform names
|
||||||
|
platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
|
||||||
|
|
||||||
|
# Sanitize player name
|
||||||
|
player = helpers.sanitize(item["player"])
|
||||||
|
|
||||||
|
row = {"id": item['id'],
|
||||||
|
"plays": item['plays'],
|
||||||
|
"last_seen": item['last_seen'],
|
||||||
|
"friendly_name": item['friendly_name'],
|
||||||
|
"ip_address": item['ip_address'],
|
||||||
|
"platform": platform,
|
||||||
|
"player": player,
|
||||||
|
"last_watched": item['last_watched'],
|
||||||
|
"thumb": thumb,
|
||||||
|
"media_type": item['media_type'],
|
||||||
|
"rating_key": item['rating_key'],
|
||||||
|
"video_decision": item['video_decision'],
|
||||||
|
"user_thumb": user_thumb,
|
||||||
|
"user": item["user"],
|
||||||
|
"user_id": item['user_id'],
|
||||||
|
"do_notify": helpers.checked(item['do_notify']),
|
||||||
|
"keep_history": helpers.checked(item['keep_history'])
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
dict = {'recordsFiltered': query['filteredCount'],
|
||||||
|
'recordsTotal': query['totalCount'],
|
||||||
|
'data': rows,
|
||||||
|
'draw': query['draw']
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict
|
||||||
|
|
||||||
|
def get_user_unique_ips(self, kwargs=None, custom_where=None):
|
||||||
|
data_tables = datatables.DataTables()
|
||||||
|
|
||||||
|
# Change custom_where column name due to ambiguous column name after JOIN
|
||||||
|
custom_where[0][0] = 'custom_user_id' if custom_where[0][0] == 'user_id' else custom_where[0][0]
|
||||||
|
|
||||||
|
columns = ['session_history.id',
|
||||||
|
'session_history.started as last_seen',
|
||||||
|
'session_history.ip_address as ip_address',
|
||||||
|
'COUNT(session_history.id) as play_count',
|
||||||
|
'session_history.platform as platform',
|
||||||
|
'session_history.player as player',
|
||||||
|
'session_history_metadata.full_title as last_watched',
|
||||||
|
'session_history_metadata.thumb',
|
||||||
|
'session_history_metadata.parent_thumb',
|
||||||
|
'session_history_metadata.grandparent_thumb',
|
||||||
|
'session_history_metadata.media_type',
|
||||||
|
'session_history.rating_key as rating_key',
|
||||||
|
'session_history_media_info.video_decision',
|
||||||
|
'session_history.user as user',
|
||||||
|
'session_history.user_id as custom_user_id',
|
||||||
|
'(case when users.friendly_name is null then users.username else \
|
||||||
|
users.friendly_name end) as friendly_name'
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = data_tables.ssp_query(table_name='session_history',
|
||||||
|
columns=columns,
|
||||||
|
custom_where=custom_where,
|
||||||
|
group_by=['ip_address'],
|
||||||
|
join_types=['JOIN',
|
||||||
|
'JOIN',
|
||||||
|
'JOIN'],
|
||||||
|
join_tables=['users',
|
||||||
|
'session_history_metadata',
|
||||||
|
'session_history_media_info'],
|
||||||
|
join_evals=[['session_history.user_id', 'users.user_id'],
|
||||||
|
['session_history.id', 'session_history_metadata.id'],
|
||||||
|
['session_history.id', 'session_history_media_info.id']],
|
||||||
|
kwargs=kwargs)
|
||||||
|
except:
|
||||||
|
logger.warn("Unable to execute database query.")
|
||||||
|
return {'recordsFiltered': 0,
|
||||||
|
'recordsTotal': 0,
|
||||||
|
'draw': 0,
|
||||||
|
'data': 'null',
|
||||||
|
'error': 'Unable to execute database query.'}
|
||||||
|
|
||||||
|
results = query['result']
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for item in results:
|
||||||
|
if item["media_type"] == 'episode' and item["parent_thumb"]:
|
||||||
|
thumb = item["parent_thumb"]
|
||||||
|
elif item["media_type"] == 'episode':
|
||||||
|
thumb = item["grandparent_thumb"]
|
||||||
|
else:
|
||||||
|
thumb = item["thumb"]
|
||||||
|
|
||||||
|
# Rename Mystery platform names
|
||||||
|
platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
|
||||||
|
|
||||||
|
# Sanitize player name
|
||||||
|
player = helpers.sanitize(item["player"])
|
||||||
|
|
||||||
|
row = {"id": item['id'],
|
||||||
|
"last_seen": item['last_seen'],
|
||||||
|
"ip_address": item['ip_address'],
|
||||||
|
"play_count": item['play_count'],
|
||||||
|
"platform": platform,
|
||||||
|
"player": player,
|
||||||
|
"last_watched": item['last_watched'],
|
||||||
|
"thumb": thumb,
|
||||||
|
"media_type": item['media_type'],
|
||||||
|
"rating_key": item['rating_key'],
|
||||||
|
"video_decision": item['video_decision'],
|
||||||
|
"friendly_name": item['friendly_name']
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
dict = {'recordsFiltered': query['filteredCount'],
|
||||||
|
'recordsTotal': query['totalCount'],
|
||||||
|
'data': rows,
|
||||||
|
'draw': query['draw']
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict
|
||||||
|
|
||||||
|
# TODO: The getter and setter for this needs to become a config getter/setter for more than just friendlyname
|
||||||
|
def set_user_friendly_name(self, user=None, user_id=None, friendly_name=None, do_notify=0, keep_history=1):
|
||||||
|
if user_id:
|
||||||
|
if friendly_name.strip() == '':
|
||||||
|
friendly_name = None
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
control_value_dict = {"user_id": user_id}
|
||||||
|
new_value_dict = {"friendly_name": friendly_name,
|
||||||
|
"do_notify": do_notify,
|
||||||
|
"keep_history": keep_history}
|
||||||
|
try:
|
||||||
|
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||||
|
except Exception, e:
|
||||||
|
logger.debug(u"Uncaught exception %s" % e)
|
||||||
|
if user:
|
||||||
|
if friendly_name.strip() == '':
|
||||||
|
friendly_name = None
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
control_value_dict = {"username": user}
|
||||||
|
new_value_dict = {"friendly_name": friendly_name,
|
||||||
|
"do_notify": do_notify,
|
||||||
|
"keep_history": keep_history}
|
||||||
|
try:
|
||||||
|
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||||
|
except Exception, e:
|
||||||
|
logger.debug(u"Uncaught exception %s" % e)
|
||||||
|
|
||||||
|
def set_user_profile_url(self, user=None, user_id=None, profile_url=None):
|
||||||
|
if user_id:
|
||||||
|
if profile_url.strip() == '':
|
||||||
|
profile_url = None
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
control_value_dict = {"user_id": user_id}
|
||||||
|
new_value_dict = {"custom_avatar_url": profile_url}
|
||||||
|
try:
|
||||||
|
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||||
|
except Exception, e:
|
||||||
|
logger.debug(u"Uncaught exception %s" % e)
|
||||||
|
if user:
|
||||||
|
if profile_url.strip() == '':
|
||||||
|
profile_url = None
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
control_value_dict = {"username": user}
|
||||||
|
new_value_dict = {"custom_avatar_url": profile_url}
|
||||||
|
try:
|
||||||
|
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||||
|
except Exception, e:
|
||||||
|
logger.debug(u"Uncaught exception %s" % e)
|
||||||
|
|
||||||
|
def get_user_friendly_name(self, user=None, user_id=None):
|
||||||
|
if user_id:
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
query = 'select username, ' \
|
||||||
|
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) as friendly_name,' \
|
||||||
|
'do_notify, keep_history, custom_avatar_url as thumb ' \
|
||||||
|
'FROM users WHERE user_id = ?'
|
||||||
|
result = monitor_db.select(query, args=[user_id])
|
||||||
|
if result:
|
||||||
|
user_detail = {'user_id': user_id,
|
||||||
|
'user': result[0]['username'],
|
||||||
|
'friendly_name': result[0]['friendly_name'],
|
||||||
|
'thumb': result[0]['thumb'],
|
||||||
|
'do_notify': helpers.checked(result[0]['do_notify']),
|
||||||
|
'keep_history': helpers.checked(result[0]['keep_history'])
|
||||||
|
}
|
||||||
|
return user_detail
|
||||||
|
else:
|
||||||
|
user_detail = {'user_id': user_id,
|
||||||
|
'user': '',
|
||||||
|
'friendly_name': '',
|
||||||
|
'do_notify': '',
|
||||||
|
'thumb': '',
|
||||||
|
'keep_history': ''}
|
||||||
|
return user_detail
|
||||||
|
elif user:
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
query = 'select user_id, ' \
|
||||||
|
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) as friendly_name,' \
|
||||||
|
'do_notify, keep_history, custom_avatar_url as thumb ' \
|
||||||
|
'FROM users WHERE username = ?'
|
||||||
|
result = monitor_db.select(query, args=[user])
|
||||||
|
if result:
|
||||||
|
user_detail = {'user_id': result[0]['user_id'],
|
||||||
|
'user': user,
|
||||||
|
'friendly_name': result[0]['friendly_name'],
|
||||||
|
'thumb': result[0]['thumb'],
|
||||||
|
'do_notify': helpers.checked(result[0]['do_notify']),
|
||||||
|
'keep_history': helpers.checked(result[0]['keep_history'])}
|
||||||
|
return user_detail
|
||||||
|
else:
|
||||||
|
user_detail = {'user_id': None,
|
||||||
|
'user': user,
|
||||||
|
'friendly_name': '',
|
||||||
|
'do_notify': '',
|
||||||
|
'thumb': '',
|
||||||
|
'keep_history': ''}
|
||||||
|
return user_detail
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_user_id(self, user=None):
|
||||||
|
if user:
|
||||||
|
try:
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
query = 'select user_id FROM users WHERE username = ?'
|
||||||
|
result = monitor_db.select_single(query, args=[user])
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_user_details(self, user=None, user_id=None):
|
||||||
|
from plexpy import plextv
|
||||||
|
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
query = 'SELECT user_id, username, friendly_name, email, ' \
|
||||||
|
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
|
||||||
|
'FROM users ' \
|
||||||
|
'WHERE username = ? ' \
|
||||||
|
'UNION ALL ' \
|
||||||
|
'SELECT null, user, null, null, null, null, null, null, null ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user = ? ' \
|
||||||
|
'GROUP BY user ' \
|
||||||
|
'LIMIT 1'
|
||||||
|
result = monitor_db.select(query, args=[user, user])
|
||||||
|
elif user_id:
|
||||||
|
query = 'SELECT user_id, username, friendly_name, email, ' \
|
||||||
|
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
|
||||||
|
'FROM users ' \
|
||||||
|
'WHERE user_id = ? ' \
|
||||||
|
'UNION ALL ' \
|
||||||
|
'SELECT user_id, user, null, null, null, null, null, null, null ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user_id = ? ' \
|
||||||
|
'GROUP BY user ' \
|
||||||
|
'LIMIT 1'
|
||||||
|
result = monitor_db.select(query, args=[user_id, user_id])
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
|
||||||
|
if result:
|
||||||
|
user_details = {}
|
||||||
|
for item in result:
|
||||||
|
if not item['friendly_name']:
|
||||||
|
friendly_name = item['username']
|
||||||
|
else:
|
||||||
|
friendly_name = item['friendly_name']
|
||||||
|
if not item['thumb'] or item['thumb'] == '':
|
||||||
|
user_thumb = common.DEFAULT_USER_THUMB
|
||||||
|
else:
|
||||||
|
user_thumb = item['thumb']
|
||||||
|
|
||||||
|
user_details = {"user_id": item['user_id'],
|
||||||
|
"username": item['username'],
|
||||||
|
"friendly_name": friendly_name,
|
||||||
|
"email": item['email'],
|
||||||
|
"thumb": user_thumb,
|
||||||
|
"is_home_user": item['is_home_user'],
|
||||||
|
"is_allow_sync": item['is_allow_sync'],
|
||||||
|
"is_restricted": item['is_restricted'],
|
||||||
|
"do_notify": item['do_notify']
|
||||||
|
}
|
||||||
|
return user_details
|
||||||
|
else:
|
||||||
|
logger.warn(u"PlexPy :: Unable to retrieve user from local database. Requesting user list refresh.")
|
||||||
|
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
|
||||||
|
if user:
|
||||||
|
# Refresh users
|
||||||
|
plextv.refresh_users()
|
||||||
|
query = 'SELECT user_id, username, friendly_name, email, ' \
|
||||||
|
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
|
||||||
|
'FROM users ' \
|
||||||
|
'WHERE username = ? ' \
|
||||||
|
'UNION ALL ' \
|
||||||
|
'SELECT null, user, null, null, null, null, null, null, null ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user = ? ' \
|
||||||
|
'GROUP BY user ' \
|
||||||
|
'LIMIT 1'
|
||||||
|
result = monitor_db.select(query, args=[user, user])
|
||||||
|
elif user_id:
|
||||||
|
# Refresh users
|
||||||
|
plextv.refresh_users()
|
||||||
|
query = 'SELECT user_id, username, friendly_name, email, ' \
|
||||||
|
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
|
||||||
|
'FROM users ' \
|
||||||
|
'WHERE user_id = ? ' \
|
||||||
|
'UNION ALL ' \
|
||||||
|
'SELECT user_id, user, null, null, null, null, null, null, null ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user_id = ? ' \
|
||||||
|
'GROUP BY user ' \
|
||||||
|
'LIMIT 1'
|
||||||
|
result = monitor_db.select(query, args=[user_id, user_id])
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
|
||||||
|
if result:
|
||||||
|
user_details = {}
|
||||||
|
for item in result:
|
||||||
|
if not item['friendly_name']:
|
||||||
|
friendly_name = item['username']
|
||||||
|
else:
|
||||||
|
friendly_name = item['friendly_name']
|
||||||
|
if not item['thumb'] or item['thumb'] == '':
|
||||||
|
user_thumb = common.DEFAULT_USER_THUMB
|
||||||
|
else:
|
||||||
|
user_thumb = item['thumb']
|
||||||
|
|
||||||
|
user_details = {"user_id": item['user_id'],
|
||||||
|
"username": item['username'],
|
||||||
|
"friendly_name": friendly_name,
|
||||||
|
"email": item['email'],
|
||||||
|
"thumb": user_thumb,
|
||||||
|
"is_home_user": item['is_home_user'],
|
||||||
|
"is_allow_sync": item['is_allow_sync'],
|
||||||
|
"is_restricted": item['is_restricted'],
|
||||||
|
"do_notify": item['do_notify']
|
||||||
|
}
|
||||||
|
return user_details
|
||||||
|
else:
|
||||||
|
# If there is no user data we must return something
|
||||||
|
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||||
|
return {"user_id": None,
|
||||||
|
"username": 'Local',
|
||||||
|
"friendly_name": 'Local',
|
||||||
|
"email": '',
|
||||||
|
"thumb": '',
|
||||||
|
"is_home_user": 0,
|
||||||
|
"is_allow_sync": 0,
|
||||||
|
"is_restricted": 0,
|
||||||
|
"do_notify": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_user_watch_time_stats(self, user=None, user_id=None):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
time_queries = [1, 7, 30, 0]
|
||||||
|
user_watch_time_stats = []
|
||||||
|
|
||||||
|
for days in time_queries:
|
||||||
|
if days > 0:
|
||||||
|
if user_id:
|
||||||
|
query = 'SELECT (SUM(stopped - started) - ' \
|
||||||
|
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||||
|
'COUNT(id) AS total_plays ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||||
|
'AND user_id = ?' % days
|
||||||
|
result = monitor_db.select(query, args=[user_id])
|
||||||
|
elif user:
|
||||||
|
query = 'SELECT (SUM(stopped - started) - ' \
|
||||||
|
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||||
|
'COUNT(id) AS total_plays ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||||
|
'AND user = ?' % days
|
||||||
|
result = monitor_db.select(query, args=[user])
|
||||||
|
else:
|
||||||
|
query = 'SELECT (SUM(stopped - started) - ' \
|
||||||
|
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
|
||||||
|
'COUNT(id) AS total_plays ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user = ?'
|
||||||
|
result = monitor_db.select(query, args=[user])
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
if item['total_time']:
|
||||||
|
total_time = item['total_time']
|
||||||
|
total_plays = item['total_plays']
|
||||||
|
else:
|
||||||
|
total_time = 0
|
||||||
|
total_plays = 0
|
||||||
|
|
||||||
|
row = {'query_days': days,
|
||||||
|
'total_time': total_time,
|
||||||
|
'total_plays': total_plays
|
||||||
|
}
|
||||||
|
|
||||||
|
user_watch_time_stats.append(row)
|
||||||
|
|
||||||
|
return user_watch_time_stats
|
||||||
|
|
||||||
|
def get_user_player_stats(self, user=None, user_id=None):
|
||||||
|
monitor_db = database.MonitorDatabase()
|
||||||
|
|
||||||
|
player_stats = []
|
||||||
|
result_id = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
if user_id:
|
||||||
|
query = 'SELECT player, COUNT(player) as player_count, platform ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user_id = ? ' \
|
||||||
|
'GROUP BY player ' \
|
||||||
|
'ORDER BY player_count DESC'
|
||||||
|
result = monitor_db.select(query, args=[user_id])
|
||||||
|
else:
|
||||||
|
query = 'SELECT player, COUNT(player) as player_count, platform ' \
|
||||||
|
'FROM session_history ' \
|
||||||
|
'WHERE user = ? ' \
|
||||||
|
'GROUP BY player ' \
|
||||||
|
'ORDER BY player_count DESC'
|
||||||
|
result = monitor_db.select(query, args=[user])
|
||||||
|
except:
|
||||||
|
logger.warn("Unable to execute database query.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
# Rename Mystery platform names
|
||||||
|
platform_type = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
|
||||||
|
|
||||||
|
row = {'player_name': item['player'],
|
||||||
|
'platform_type': platform_type,
|
||||||
|
'total_plays': item['player_count'],
|
||||||
|
'result_id': result_id
|
||||||
|
}
|
||||||
|
player_stats.append(row)
|
||||||
|
result_id += 1
|
||||||
|
|
||||||
|
return player_stats
|
||||||
@@ -1 +1,2 @@
|
|||||||
PLEXPY_VERSION = "master"
|
PLEXPY_VERSION = "master"
|
||||||
|
PLEXPY_RELEASE_VERSION = "1.2.10"
|
||||||
|
|||||||
@@ -241,3 +241,37 @@ def update():
|
|||||||
e
|
e
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def read_changelog():
|
||||||
|
|
||||||
|
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
||||||
|
|
||||||
|
try:
|
||||||
|
logfile = open(changelog_file, "r")
|
||||||
|
except IOError, e:
|
||||||
|
logger.error('PlexPy Version Checker :: Unable to open changelog file. %s' % e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if logfile:
|
||||||
|
output = ''
|
||||||
|
lines = logfile.readlines()
|
||||||
|
previous_line = ''
|
||||||
|
for line in lines:
|
||||||
|
if line[:2] == '# ':
|
||||||
|
output += '<h3>' + line[2:] + '</h3>'
|
||||||
|
elif line[:3] == '## ':
|
||||||
|
output += '<h4>' + line[3:] + '</h4>'
|
||||||
|
elif line[:2] == '* ' and previous_line.strip() == '':
|
||||||
|
output += '<ul><li>' + line[2:] + '</li>'
|
||||||
|
elif line[:2] == '* ':
|
||||||
|
output += '<li>' + line[2:] + '</li>'
|
||||||
|
elif line.strip() == '' and previous_line[:2] == '* ':
|
||||||
|
output += '</ul></br>'
|
||||||
|
else:
|
||||||
|
output += line + '</br>'
|
||||||
|
|
||||||
|
previous_line = line
|
||||||
|
|
||||||
|
return output
|
||||||
|
else:
|
||||||
|
return '<h4>No changelog data</h4>'
|
||||||
152
plexpy/web_socket.py
Normal file
152
plexpy/web_socket.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# PlexPy is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# PlexPy is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# Mostly borrowed from https://github.com/trakt/Plex-Trakt-Scrobbler
|
||||||
|
|
||||||
|
from plexpy import logger, activity_pinger
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import plexpy
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
name = 'websocket'
|
||||||
|
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
|
|
||||||
|
def start_thread():
|
||||||
|
# Check for any existing sessions on start up
|
||||||
|
activity_pinger.check_active_sessions(ws_request=True)
|
||||||
|
# Start the websocket listener on it's own thread
|
||||||
|
threading.Thread(target=run).start()
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
from websocket import create_connection
|
||||||
|
|
||||||
|
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||||
|
plexpy.CONFIG.PMS_IP,
|
||||||
|
plexpy.CONFIG.PMS_PORT
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set authentication token (if one is available)
|
||||||
|
if plexpy.CONFIG.PMS_TOKEN:
|
||||||
|
uri += '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
|
ws_connected = False
|
||||||
|
reconnects = 0
|
||||||
|
|
||||||
|
# Try an open the websocket connection - if it fails after 15 retries fallback to polling
|
||||||
|
while not ws_connected and reconnects <= 15:
|
||||||
|
try:
|
||||||
|
logger.info(u'PlexPy WebSocket :: Opening websocket, connection attempt %s.' % str(reconnects + 1))
|
||||||
|
ws = create_connection(uri)
|
||||||
|
reconnects = 0
|
||||||
|
ws_connected = True
|
||||||
|
logger.info(u'PlexPy WebSocket :: Ready')
|
||||||
|
except IOError, e:
|
||||||
|
logger.error(u'PlexPy WebSocket :: %s.' % e)
|
||||||
|
reconnects += 1
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
while ws_connected:
|
||||||
|
try:
|
||||||
|
process(*receive(ws))
|
||||||
|
|
||||||
|
# successfully received data, reset reconnects counter
|
||||||
|
reconnects = 0
|
||||||
|
except websocket.WebSocketConnectionClosedException:
|
||||||
|
if reconnects <= 15:
|
||||||
|
reconnects += 1
|
||||||
|
|
||||||
|
# Sleep 5 between connection attempts
|
||||||
|
if reconnects > 1:
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
logger.warn(u'PlexPy WebSocket :: Connection has closed, reconnecting...')
|
||||||
|
try:
|
||||||
|
ws = create_connection(uri)
|
||||||
|
except IOError, e:
|
||||||
|
logger.info(u'PlexPy WebSocket :: %s.' % e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
ws_connected = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if not ws_connected:
|
||||||
|
logger.error(u'PlexPy WebSocket :: Connection unavailable, falling back to polling.')
|
||||||
|
plexpy.POLLING_FAILOVER = True
|
||||||
|
plexpy.initialize_scheduler()
|
||||||
|
|
||||||
|
logger.debug(u'PlexPy WebSocket :: Leaving thread.')
|
||||||
|
|
||||||
|
|
||||||
|
def receive(ws):
|
||||||
|
frame = ws.recv_frame()
|
||||||
|
|
||||||
|
if not frame:
|
||||||
|
raise websocket.WebSocketException("Not a valid frame %s" % frame)
|
||||||
|
elif frame.opcode in opcode_data:
|
||||||
|
return frame.opcode, frame.data
|
||||||
|
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
|
||||||
|
ws.send_close()
|
||||||
|
return frame.opcode, None
|
||||||
|
elif frame.opcode == websocket.ABNF.OPCODE_PING:
|
||||||
|
ws.pong("Hi!")
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def process(opcode, data):
|
||||||
|
from plexpy import activity_handler
|
||||||
|
|
||||||
|
if opcode not in opcode_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = json.loads(data)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warn(u'PlexPy WebSocket :: Error decoding message from websocket: %s' % ex)
|
||||||
|
logger.debug(data)
|
||||||
|
return False
|
||||||
|
|
||||||
|
type = info.get('type')
|
||||||
|
|
||||||
|
if not type:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if type == 'playing':
|
||||||
|
# logger.debug('%s.playing %s' % (name, info))
|
||||||
|
try:
|
||||||
|
time_line = info.get('_children')
|
||||||
|
except:
|
||||||
|
logger.debug(u"PlexPy WebSocket :: Session found but unable to get timeline data.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
activity = activity_handler.ActivityHandler(timeline=time_line[0])
|
||||||
|
activity.process()
|
||||||
|
|
||||||
|
#if type == 'timeline':
|
||||||
|
# try:
|
||||||
|
# time_line = info.get('_children')
|
||||||
|
# except:
|
||||||
|
# logger.debug(u"PlexPy WebSocket :: Timeline event found but unable to get timeline data.")
|
||||||
|
# return False
|
||||||
|
|
||||||
|
# activity = activity_handler.TimelineHandler(timeline=time_line[0])
|
||||||
|
# activity.process()
|
||||||
|
|
||||||
|
return True
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user