Compare commits
490 Commits
dev-branch
...
master
Author | SHA1 | Date | |
---|---|---|---|
e097a07ca5 | |||
5bcdf4fe43 | |||
5af2e6a7ae | |||
c30850673a | |||
9f4d7db7d2 | |||
73bf20d86d | |||
a3d3e6c295 | |||
02c09389dd | |||
f28a52d727 | |||
c52df7c5ee | |||
9a8cde23bd | |||
8e68e8ce7e | |||
078bbccc54 | |||
b2ceefe975 | |||
c19b05cb4d | |||
f8a95935f9 | |||
4d162d849d | |||
a04b1a3c32 | |||
10287e3885 | |||
62e7bbc209 | |||
e6637bf3b7 | |||
e065626cdd | |||
76b2a7274a | |||
9ae6b9de7c | |||
294a88bd19 | |||
501be17842 | |||
5cb980bf6c | |||
3a2f9d5b12 | |||
569f24ee7c | |||
135d94a283 | |||
c68de7ac66 | |||
bef56becde | |||
5a2c50d1be | |||
37d651605b | |||
2ca924e085 | |||
39560ce6f5 | |||
536da598bd | |||
1d6dece7be | |||
a4dc39e706 | |||
111357934e | |||
ef464a1dbf | |||
cfe4182f0a | |||
8ede33d83d | |||
3e1af802fa | |||
1f360bd6da | |||
cc6e98b7b2 | |||
a2ec53e4a3 | |||
bfaeed0736 | |||
0a1e19b18f | |||
df09d109a8 | |||
46afeff95c | |||
31e100835c | |||
b2f420b6fb | |||
07b0c942ab | |||
58c8501529 | |||
39fbaa3ade | |||
d62a7e9715 | |||
012a9fd4f6 | |||
a81ff1fdf9 | |||
f706e57701 | |||
bf605ab628 | |||
dbdbf2ad4a | |||
58a962a855 | |||
0a831fa07e | |||
4305a1190b | |||
9a16f5ff28 | |||
0de8004ea0 | |||
1399208ca3 | |||
fad1c309a7 | |||
57afb59f0b | |||
c84cee5fec | |||
a479596b35 | |||
42bd315c1e | |||
3204a0cd44 | |||
d076426bf7 | |||
8c6689796c | |||
126f14bd3d | |||
4125214b66 | |||
d10c6cf94f | |||
72fd5c4e51 | |||
29ab0ebaa4 | |||
08f872b35c | |||
bf8bd61e17 | |||
69fd40e25a | |||
973adc0148 | |||
02a8831fb5 | |||
57b91ff139 | |||
4a67ece43d | |||
e55c7db5da | |||
f1ed5fb174 | |||
96831b5990 | |||
40131f5c20 | |||
5dafd7eb93 | |||
a238742b1b | |||
9af6626b6d | |||
0aa5abd6c8 | |||
592029cee3 | |||
5a41abd974 | |||
3421661e22 | |||
37aab68400 | |||
12eb897ad0 | |||
0125865124 | |||
7f20da0ca9 | |||
ce8610d0ed | |||
06589274bf | |||
ebfbc51f72 | |||
3036a126ba | |||
639c0395c2 | |||
557aabefb8 | |||
4632dfc600 | |||
8e4e58792d | |||
2369390c4d | |||
547e2c45b3 | |||
e54372379b | |||
84efcc631f | |||
8c302e4187 | |||
8a46b7a237 | |||
c24fcc980b | |||
573651d31b | |||
0ecd6c097f | |||
65285d4b3a | |||
2b776137a4 | |||
fe33f6d9e0 | |||
abff846216 | |||
244e8ad71c | |||
41ed4199e1 | |||
26a37f456a | |||
fc2fba2f2b | |||
29b53dbd4b | |||
e5a1f7fba0 | |||
5dcc0cb515 | |||
0107ec6253 | |||
68e53f6f9d | |||
a1adb47767 | |||
e24114ad7a | |||
28879b77d4 | |||
aa265463ce | |||
29d328742e | |||
3e117480e3 | |||
77a7614730 | |||
c9d369cdf4 | |||
f5c2958b90 | |||
928fd3f194 | |||
f105c0a9c8 | |||
0d1685afed | |||
dc6507ffed | |||
30dbccda77 | |||
61524c4b92 | |||
f90497fbd2 | |||
5bf7925585 | |||
36aa2dd3a3 | |||
34851652c5 | |||
361610707b | |||
0e93543c0d | |||
7b48654c80 | |||
56e2239c5a | |||
bf624c6d7b | |||
f38f1ee7af | |||
edfa7315a0 | |||
87b82a5947 | |||
fe490699f9 | |||
d7e1e909d1 | |||
9705be3a94 | |||
41051a826c | |||
49a15fa3fa | |||
ad2120203c | |||
a8d8b1b66f | |||
b95ac52e00 | |||
daee5676ed | |||
034d772ac0 | |||
4a7d69e620 | |||
aac5518ddf | |||
d38ee9c341 | |||
7dde481636 | |||
c160b66e3f | |||
844e780cd0 | |||
8ad42fb3e7 | |||
86b471b5d2 | |||
5f942142f3 | |||
0100aa4246 | |||
7e47ee66e4 | |||
a5c5ef3710 | |||
98f4fe0742 | |||
1073642054 | |||
a7286fb811 | |||
9d5143ab56 | |||
bc3f41e0ef | |||
ca38dc30a5 | |||
0b6f3c9a6b | |||
c38138b41b | |||
a8e3de7aae | |||
ad47db6587 | |||
ff0e834b9c | |||
7b0befad71 | |||
71c2208fee | |||
514151e69b | |||
22ff42a373 | |||
1a892fe750 | |||
674095f5ba | |||
6d7285cb51 | |||
837e733ca2 | |||
9f680cf0ef | |||
8a80b7b3ac | |||
55740a099a | |||
720dc4b7b4 | |||
0f8cbc77ef | |||
78c644a20c | |||
66ac37b34e | |||
83bdf49148 | |||
5f99b37cfe | |||
645c43708a | |||
f7f0e19394 | |||
bac3e88ad8 | |||
65b424d3cc | |||
c5f29ac39f | |||
b5fd014c17 | |||
82c7704fab | |||
4ac7313ff0 | |||
cd809ba2f0 | |||
dce0dcfd23 | |||
1cbdb149f7 | |||
be28995340 | |||
438673df66 | |||
5d8d40d6ee | |||
85f9dac29e | |||
e73dca7557 | |||
1fcee2d18f | |||
245d1d61c4 | |||
8cbdf2de55 | |||
11bd4b1e9f | |||
63a0427d91 | |||
852e355a36 | |||
541dd56901 | |||
b9c2ea66f2 | |||
c80682787b | |||
740dc498e2 | |||
165e851cbc | |||
f011d53d8c | |||
60772450a0 | |||
2d2d88f58c | |||
1a13d98cd2 | |||
12a6875af9 | |||
f9e4ce2ff3 | |||
f37d735e24 | |||
d341092675 | |||
2be0e376f8 | |||
ca5f4fb271 | |||
caebf46e83 | |||
8edde2242f | |||
672a2abaf4 | |||
0689a760d8 | |||
9a28bbf9cb | |||
f8c75a3696 | |||
2ab33748cd | |||
1e18a21d26 | |||
4bca83c77a | |||
81eac90620 | |||
65cc36e9a4 | |||
db5c497cbd | |||
d46b6ee316 | |||
56a84f19dd | |||
f2458c1089 | |||
983d49ea8e | |||
1952413639 | |||
94128a92cb | |||
5179aa7175 | |||
a7a6a9381d | |||
5ddd981f2f | |||
2a2a9f74ff | |||
5e161997dc | |||
47a8c1ecb4 | |||
149ba7f873 | |||
ac0db8b69f | |||
517ffbc678 | |||
b2bfb400a6 | |||
018b4faa93 | |||
ed65e4e948 | |||
ba5335c1f0 | |||
54bdba174c | |||
343c45d78e | |||
4739ae9615 | |||
7db5c68061 | |||
f626a9daa1 | |||
7128597beb | |||
3811bfa5fc | |||
bcb85978f0 | |||
a8bd260f33 | |||
6cd1231ec5 | |||
926e72a685 | |||
1ed9a98bdd | |||
a9d2225de0 | |||
2a4c6f9a22 | |||
2258b21b2c | |||
f35c1a23b0 | |||
45185d9ad8 | |||
aa4448de15 | |||
ce4b279fe7 | |||
5b4feb6208 | |||
d90d052696 | |||
599d9970ea | |||
0321b883eb | |||
5ae2e9f6d7 | |||
ef87f88e05 | |||
aee131548b | |||
8f02098655 | |||
e605979a16 | |||
80557f4a61 | |||
1adcc9e41b | |||
e45ff63d9b | |||
cbc71bfc1a | |||
ebc5151fbc | |||
c603efb472 | |||
e4d0eaf5b1 | |||
c3fa69ba28 | |||
6e0dd60a84 | |||
d2b37837d8 | |||
4c014cfc24 | |||
88f775e646 | |||
625948b73e | |||
0c815e459b | |||
6c4d0039e2 | |||
8651dc34dd | |||
13099bf557 | |||
769acfff01 | |||
34f161de22 | |||
e667d48f68 | |||
c9c57352f4 | |||
58e7fd1335 | |||
11715e1e6e | |||
ca19d3d6b4 | |||
3d632aa65e | |||
b5c7261c3b | |||
8ef3e5fc82 | |||
1409209e58 | |||
70accd1767 | |||
fcdb0dbc3d | |||
f3389a1c22 | |||
b5b77963fe | |||
bcec98f189 | |||
c1865f77de | |||
7818058f16 | |||
2cb51cc312 | |||
91d7195143 | |||
bc9d1bc550 | |||
63298cbb63 | |||
773e09d730 | |||
83990a7620 | |||
472e438967 | |||
f139b19d35 | |||
baa9ff80fe | |||
3b7d5216aa | |||
bb674a413d | |||
d3e6d85290 | |||
f36f7a7ec0 | |||
d9e383f871 | |||
0afd3bf40b | |||
75665d50a2 | |||
6fcf430152 | |||
99eee6fdfb | |||
640a1f70dc | |||
bfdf94fc14 | |||
29566aa47a | |||
8fc1e7dbfb | |||
4bb37d0073 | |||
aff964522b | |||
a27056a11f | |||
79088cf6ce | |||
93ddee995e | |||
d1443d2670 | |||
758af21ade | |||
457486924e | |||
f9ce8eae63 | |||
463586ff29 | |||
b5347e3304 | |||
6d418e54f7 | |||
aa6017374e | |||
72fbf36377 | |||
64f2312248 | |||
e222525d14 | |||
dd7228904b | |||
316616f02a | |||
baa687355f | |||
e2475fae3b | |||
2a39697acd | |||
542f58b510 | |||
473808d720 | |||
f7a7125783 | |||
f6faa5fe1a | |||
2e85fdc72e | |||
63ef06e814 | |||
a162cec74d | |||
534dfe4aeb | |||
92c96ae59d | |||
591c299af6 | |||
27db3a6eaa | |||
b5c40b29d2 | |||
0a445d1ab3 | |||
7cdb1355f5 | |||
eeed9e6f81 | |||
11c5bb9ead | |||
29ad54f844 | |||
0ce14ada68 | |||
a7cf9c784b | |||
17baec0e5d | |||
dbb03e595f | |||
69f88ef738 | |||
84c843f54f | |||
6a746a6700 | |||
a0a634fb3e | |||
a3c3a145f1 | |||
797a445679 | |||
8087742f33 | |||
cda132b2d9 | |||
d32f36f51e | |||
8dbd187ef7 | |||
14ea3bb0d5 | |||
f9f8cf401a | |||
222ce6cd60 | |||
1e127c6940 | |||
f41501e77c | |||
08e68b896d | |||
2a8f18405f | |||
c1f68df4f5 | |||
02cc9878a6 | |||
19e85e6dbb | |||
3f2e3798ad | |||
bcc7a358a7 | |||
18bd6d8461 | |||
15a262339d | |||
24360642f7 | |||
c96596f422 | |||
78d196c4df | |||
85ada26e3d | |||
c34736f124 | |||
69d1c7f8eb | |||
46c88b9baf | |||
5d28f595e4 | |||
0c2d97cad4 | |||
6fff1c61df | |||
467ae80e02 | |||
e31160d634 | |||
160277dd62 | |||
8fffc30c77 | |||
713f6fbe56 | |||
0987874f44 | |||
1a9fdd1a99 | |||
286c65f9a2 | |||
5b58e94ab8 | |||
0c0ee06125 | |||
ce8f3838c0 | |||
9f45bed626 | |||
c3063a77e4 | |||
d802c334fe | |||
d20ecda1ee | |||
6f30b44431 | |||
18526cf765 | |||
8d7f0fc11b | |||
faf959fa4a | |||
5ea6211956 | |||
59675d4a5b | |||
1635da7619 | |||
793092ddc8 | |||
8caff8cdc0 | |||
a78244df75 | |||
4782bd9e5e | |||
c5778c89a3 | |||
e39c35ab87 | |||
a336c3aded | |||
ea347159d6 | |||
d5053cdca8 | |||
52d42ab46c | |||
b2bcce67ef | |||
6f40d31f9d | |||
dd33595b98 | |||
e9a085c28f | |||
038c17c41d | |||
ec07cebef9 | |||
c474182b97 | |||
9e35fbe091 | |||
587cf32087 | |||
970535057d | |||
8c3781728a | |||
ffa97daeff | |||
96adf71620 | |||
c3978ace6b | |||
52b91ee9c6 | |||
66d7cd3aa5 | |||
124e324c95 | |||
f25a881a27 | |||
3cd993aaad |
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: Issue
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Provide logs**
|
||||
|
||||
Tautulli logs - Please follow [Tautulli's guide](https://github.com/Tautulli/Tautulli-Wiki/wiki/Asking-for-Support#how-can-i-share-my-logs) for sharing logs. Please provide a link to a gist or pastebin.
|
||||
|
||||
PlexAPI logs - Create or find your logs location for [PlexAPI](https://python-plexapi.readthedocs.io/en/latest/configuration.html). Please follow [Tautulli's guide](https://github.com/Tautulli/Tautulli-Wiki/wiki/Asking-for-Support#how-can-i-share-my-logs) for sharing logs. Please provide a link to a gist or pastebin.
|
||||
|
||||
**Link to script with bug/issue**
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Linux, Windows, Docker]
|
||||
- Python Version [e.g. 22]
|
||||
- PlexAPI version [e.g. 4.1.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: Request
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request an improvement on an existing script? Please link to script.**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
60
README.md
60
README.md
@ -17,14 +17,14 @@ print(plexapi.CONFIG_PATH)
|
||||
[](https://discord.gg/tQcWEUp) [](https://www.reddit.com/user/Blacktwin/) [](https://forums.plex.tv/u/blacktwin) [](https://github.com/blacktwin/JBOPS/issues/new)
|
||||
|
||||
### Donation
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4J6RPWZ9J9YML)
|
||||
|
||||
|
||||
<a href="https://www.paypal.me/Adam581/1" target=blank><img src=http://imgur.com/WSVZSTW.png alt="Buy Me a Coffee" height=50 width=100 align='center'>
|
||||
</a> or <a href="https://www.paypal.me/Adam581/3" target=blank><img src=http://imgur.com/gnvlm6n.jpg alt="Buy Me a Beer" height=50 width=100 align='center'></a>
|
||||
|
||||
<details>
|
||||
<summary>Coins?</summary>
|
||||
<details>
|
||||
<summary>BTC:</summary>
|
||||
FCb4F3bv1hHCJxq6HJMQiAMn883v3okdh
|
||||
3FCb4F3bv1hHCJxq6HJMQiAMn883v3okdh
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -78,9 +78,9 @@ Scripts pulled from my gist profile.
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://gist.github.com/blacktwin/397f07724abebd1223ba6ea644ea1669"><img src="https://img.shields.io/badge/gist-original-green.svg"></a></td>
|
||||
<td><a href="../master/fun/aired_today_playlist.py">aired_today_playlist</a></td>
|
||||
<td>Create a Plex Playlist with what was aired on this today's month-day, sort by oldest first. If Playlist from yesterday exists delete and create today's. If today's Playlist exists exit.</td>
|
||||
<td></td>
|
||||
<td><a href="../master/fun/playlist_manager.py">playlist_manager</a></td>
|
||||
<td>Create and share playlists based on Most Popular TV/Movies from Tautulli and Aired this day in history.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://gist.github.com/blacktwin/4ccb79c7d01a95176b8e88bf4890cd2b"><img src="https://img.shields.io/badge/gist-original-green.svg"></a></td>
|
||||
@ -105,18 +105,9 @@ Killing streams is a Plex Pass feature. These scripts will only work for Plex Pa
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a href="../master/killstream/watch_limit.py">watch_limit</a></td>
|
||||
<td>Kill streams if user has watched too much Plex Today.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a href="../master/killstream/play_limit.py">play_limit</a></td>
|
||||
<td>Kill streams if user has played too much Plex Today.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a href="../master/killstream/kill_time.py">kill_time</a></td>
|
||||
<td>Limit number of plays of TV Show episodes during time of day. Idea is to reduce continuous plays while sleeping.</td>
|
||||
<td><a href="../master/killstream/limiterr.py">limiterr</a></td>
|
||||
<td>Limiting Plex users by plays, watches, or total time from Tautulli..
|
||||
See killsteam section <a href="../master/killstream/limiterr_readme.md">limiterr_readme.md</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
@ -251,11 +242,6 @@ Killing streams is a Plex Pass feature. These scripts will only work for Plex Pa
|
||||
<td><a href="../master/utility/plexapi_delete_playlists.py">plexapi_delete_playlists</a></td>
|
||||
<td>Delete all playlists from Plex using PlexAPI. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://gist.github.com/blacktwin/df58032de3e6f4d29f7ea562aeaebbab"><img src="https://img.shields.io/badge/gist-original-green.svg"></a></td>
|
||||
<td><a href="../master/utility/plexapi_search_file.py">plexapi_search_file</a></td>
|
||||
<td>Find full path for Plex items. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://gist.github.com/blacktwin/3752a76fa0b3fc6d19e842af7b812184"><img src="https://img.shields.io/badge/gist-original-green.svg"></a></td>
|
||||
<td><a href="../master/utility/refresh_next_episode.py">refresh_next_episode</a></td>
|
||||
@ -318,17 +304,22 @@ Killing streams is a Plex Pass feature. These scripts will only work for Plex Pa
|
||||
</details>
|
||||
|
||||
----
|
||||
|
||||
### Setting Up Tautulli for Custom Scripts
|
||||
<details>
|
||||
<summary>Setting Up Tautulli for Custom Scripts</summary>
|
||||
<summary></summary>
|
||||
|
||||
#### Enable API in Tautulli:
|
||||
|
||||
Tautulli > Settings > Web Interface > API > Enable API
|
||||
|
||||
|
||||
#### Enabling Scripts in Tautulli:
|
||||
|
||||
Taultulli > Settings > Notification Agents > Add a Notification Agent > Script
|
||||
Tautulli > Settings > Notification Agents > Add a Notification Agent > Script
|
||||
|
||||
#### Configuration
|
||||
|
||||
Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Tautulli > Settings > Notification Agents > New Script > Configuration:
|
||||
- [ ] Set scripts location to location of your script
|
||||
- [ ] Scroll down to option you want to use and select the script from the drop down menu
|
||||
- [ ] Set desired Script Timeout value
|
||||
@ -336,21 +327,21 @@ Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
- [ ] Save
|
||||
|
||||
#### Triggers
|
||||
Taultulli > Settings > Notification Agents > New Script > Triggers:
|
||||
Tautulli > Settings > Notification Agents > New Script > Triggers:
|
||||
|
||||
- [ ] Check desired trigger
|
||||
- [ ] Save
|
||||
|
||||
#### Conditions
|
||||
Taultulli > Settings > Notification Agents > New Script > Conditions:
|
||||
Tautulli > Settings > Notification Agents > New Script > Conditions:
|
||||
|
||||
- [ ] Set desired conditions
|
||||
- [ ] Save
|
||||
|
||||
For more information on Tautulli conditions see [here](https://github.com/Tautulli/Tautulli-Wiki/wiki/Custom-Notification-Conditions)
|
||||
For more information on Tautulli conditions see [here](https://github.com/Tautulli/Tautulli/wiki/Custom-Notification-Conditions)
|
||||
|
||||
#### Script Arguments
|
||||
Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
Tautulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
- [ ] Select desired trigger
|
||||
- [ ] Input desired notification parameters (List of parameters will likely be found inside script)
|
||||
@ -361,8 +352,7 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
</details>
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary>Common variables</summary>
|
||||
### Common variables
|
||||
|
||||
<details>
|
||||
<summary>Plex</summary>
|
||||
@ -377,5 +367,3 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
- [ ] TAUTULLI_URL - Local/Remote IP to connect to Tautulli ('http://localhost:8181', 'https://x.x.x.x:8182', etc.)
|
||||
- [ ] TAUTULLI_APIKEY - Tautulli Settings > Access Control > Enable API - API Key
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
@ -1,76 +0,0 @@
|
||||
"""
|
||||
Create a Plex Playlist with what aired on this day in history (month-day), sort by oldest first.
|
||||
If Playlist from yesterday exists delete and create today's.
|
||||
If today's Playlist exists exit.
|
||||
"""
|
||||
|
||||
import operator
|
||||
from plexapi.server import PlexServer
|
||||
import requests
|
||||
import datetime
|
||||
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxx'
|
||||
|
||||
LIBRARY_NAMES = ['Movies', 'TV Shows'] # Your library names
|
||||
|
||||
today = datetime.datetime.now().date()
|
||||
|
||||
TODAY_PLAY_TITLE = 'Aired Today {}-{}'.format(today.month, today.day)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
def remove_old():
|
||||
# Remove old Aired Today Playlists
|
||||
for playlist in plex.playlists():
|
||||
if playlist.title.startswith('Aired Today') and playlist.title != TODAY_PLAY_TITLE:
|
||||
playlist.delete()
|
||||
print('Removing old Aired Today Playlists: {}'.format(playlist.title))
|
||||
elif playlist.title == TODAY_PLAY_TITLE:
|
||||
print('{} already exists. No need to make again.'.format(TODAY_PLAY_TITLE))
|
||||
exit(0)
|
||||
|
||||
|
||||
def get_all_content(library_name):
|
||||
# Get all movies or episodes from LIBRARY_NAME
|
||||
child_lst = []
|
||||
for library in library_name:
|
||||
for child in plex.library.section(library).all():
|
||||
if child.type == 'movie':
|
||||
child_lst += [child]
|
||||
elif child.type == 'show':
|
||||
child_lst += child.episodes()
|
||||
else:
|
||||
pass
|
||||
return child_lst
|
||||
|
||||
|
||||
def find_air_dates(content_lst):
|
||||
# Find what aired with today's month-day
|
||||
aired_lst = []
|
||||
for video in content_lst:
|
||||
try:
|
||||
ad_month = str(video.originallyAvailableAt.month)
|
||||
ad_day = str(video.originallyAvailableAt.day)
|
||||
|
||||
if ad_month == str(today.month) and ad_day == str(today.day):
|
||||
aired_lst += [[video] + [str(video.originallyAvailableAt)]]
|
||||
except Exception as e:
|
||||
# print(e)
|
||||
pass
|
||||
|
||||
# Sort by original air date, oldest first
|
||||
aired_lst = sorted(aired_lst, key=operator.itemgetter(1))
|
||||
|
||||
# Remove date used for sorting
|
||||
play_lst = [x[0] for x in aired_lst]
|
||||
return play_lst
|
||||
|
||||
|
||||
remove_old()
|
||||
play_lst = find_air_dates(get_all_content(LIBRARY_NAMES))
|
||||
# Create Playlist
|
||||
if play_lst:
|
||||
plex.createPlaylist(TODAY_PLAY_TITLE, play_lst)
|
||||
else:
|
||||
print('Found nothing aired on this day in history.')
|
1032
fun/playlist_manager.py
Normal file
1032
fun/playlist_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Author: Bailey Belvis (https://github.com/philosowaffle)
|
||||
#
|
||||
@ -6,18 +9,16 @@
|
||||
#
|
||||
# - Enable `Upload Posters to Imgur for Notifications` - required for lights to match the posters color scheme
|
||||
# - Triggers - PlexLifx supports the following triggers, enable the ones you are interested in.
|
||||
# - Notify on Playback Start
|
||||
# - Notify on Playback Stop
|
||||
# - Notify on Playback Resume
|
||||
# - Notify on Playback Pause
|
||||
# - Notify on Playback Start
|
||||
# - Notify on Playback Stop
|
||||
# - Notify on Playback Resume
|
||||
# - Notify on Playback Pause
|
||||
#
|
||||
# - Copy paste the following line to each of the Triggers you enabled (found on the Arguments tab):
|
||||
# -a {action} -mt {media_type} -mi {machine_id} -rk {rating_key} -pu {poster_url}
|
||||
#
|
||||
# -a {action} -mt {media_type} -mi {machine_id} -rk {rating_key} -pu {poster_url}
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import hashlib
|
||||
import shutil
|
||||
import numpy
|
||||
import argparse
|
||||
@ -51,8 +52,8 @@ Duration = 3.0
|
||||
# Number of colors to be used across your lights
|
||||
NumColors = 5
|
||||
|
||||
# How closely the colors should match the media thumbnail, 10 is the highest
|
||||
ColorQuality = 10
|
||||
# How closely the colors should match the media thumbnail, 1 is the best match, larger numbers will perform faster but may be a less accurate color match
|
||||
ColorQuality = 1
|
||||
|
||||
# Default theme to restore lights to on media pause/stop
|
||||
DefaultPauseTheme = "Basic"
|
||||
@ -99,10 +100,10 @@ filtered_players = [] if PlayerUUIDs == "none" else PlayerUUIDs.split(',')
|
||||
logger.debug("Filtered Players: " + filtered_players.__str__())
|
||||
|
||||
events = [
|
||||
'play',
|
||||
'pause',
|
||||
'resume',
|
||||
'stop'
|
||||
'play',
|
||||
'pause',
|
||||
'resume',
|
||||
'stop'
|
||||
]
|
||||
|
||||
##############################
|
||||
@ -115,33 +116,33 @@ num_colors = NumColors if NumColors else 4
|
||||
color_quality = ColorQuality if ColorQuality else 1
|
||||
|
||||
if not APIKey:
|
||||
logger.error("Missing LIFX API Key")
|
||||
exit(1)
|
||||
logger.error("Missing LIFX API Key")
|
||||
exit(1)
|
||||
else:
|
||||
lifx_api_key = APIKey
|
||||
logger.debug("LIFX API Key: " + lifx_api_key)
|
||||
lifx_api_key = APIKey
|
||||
logger.debug("LIFX API Key: " + lifx_api_key)
|
||||
|
||||
pifx = PIFX(lifx_api_key)
|
||||
|
||||
lights = []
|
||||
if Lights:
|
||||
lights_use_name = True
|
||||
lights = Lights.split(',')
|
||||
lights_use_name = True
|
||||
lights = Lights.split(',')
|
||||
|
||||
tmp = []
|
||||
for light in lights:
|
||||
tmp.append(light.strip())
|
||||
lights = tmp
|
||||
tmp = []
|
||||
for light in lights:
|
||||
tmp.append(light.strip())
|
||||
lights = tmp
|
||||
else:
|
||||
lights_detail = pifx.list_lights()
|
||||
for light in lights_detail:
|
||||
lights.append(light['id'])
|
||||
shuffle(lights)
|
||||
lights_detail = pifx.list_lights()
|
||||
for light in lights_detail:
|
||||
lights.append(light['id'])
|
||||
shuffle(lights)
|
||||
|
||||
scenes_details = pifx.list_scenes()
|
||||
scenes = dict()
|
||||
for scene in scenes_details:
|
||||
scenes[scene['name']] = scene['uuid']
|
||||
scenes[scene['name']] = scene['uuid']
|
||||
|
||||
logger.debug(scenes)
|
||||
logger.debug(lights)
|
||||
@ -154,7 +155,7 @@ default_play_uuid = scenes[default_play_theme]
|
||||
|
||||
number_of_lights = len(lights)
|
||||
if number_of_lights < num_colors:
|
||||
num_colors = number_of_lights
|
||||
num_colors = number_of_lights
|
||||
|
||||
light_groups = numpy.array_split(numpy.array(lights), num_colors)
|
||||
|
||||
@ -167,15 +168,15 @@ logger.debug("Color Quality: " + color_quality.__str__())
|
||||
##############################
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('-a', '--action', action='store', default='',
|
||||
help='The action that triggered the script.')
|
||||
help='The action that triggered the script.')
|
||||
p.add_argument('-mt', '--media_type', action='store', default='',
|
||||
help='The media type of the media being played.')
|
||||
help='The media type of the media being played.')
|
||||
p.add_argument('-mi', '--machine_id', action='store', default='',
|
||||
help='The machine id of where the media is playing.')
|
||||
help='The machine id of where the media is playing.')
|
||||
p.add_argument('-rk', '--rating_key', action='store', default='',
|
||||
help='The unique identifier for the media.')
|
||||
help='The unique identifier for the media.')
|
||||
p.add_argument('-pu', '--poster_url', action='store', default='',
|
||||
help='The poster url for the media playing.')
|
||||
help='The poster url for the media playing.')
|
||||
|
||||
parser = p.parse_args()
|
||||
|
||||
@ -196,22 +197,22 @@ logger.debug("Media Guid: " + media_guid)
|
||||
logger.debug("Poster Url: " + poster_url)
|
||||
|
||||
# Only perform action for event play/pause/resume/stop for TV and Movies
|
||||
if not event in events:
|
||||
logger.debug("Invalid action: " + event)
|
||||
exit()
|
||||
if event not in events:
|
||||
logger.debug("Invalid action: " + event)
|
||||
exit()
|
||||
|
||||
if (media_type != "movie") and (media_type != "episode"):
|
||||
logger.debug("Media type was not movie or episode, ignoring.")
|
||||
exit()
|
||||
logger.debug("Media type was not movie or episode, ignoring.")
|
||||
exit()
|
||||
|
||||
# If we configured only specific players to be able to play with the lights
|
||||
if filtered_players:
|
||||
try:
|
||||
if player_uuid not in filtered_players:
|
||||
logger.info(player_uuid + " player is not able to play with the lights")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.error("Failed to check uuid - " + e.__str__())
|
||||
try:
|
||||
if player_uuid not in filtered_players:
|
||||
logger.info(player_uuid + " player is not able to play with the lights")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.error("Failed to check uuid - " + e.__str__())
|
||||
|
||||
# Setup Thumbnail directory paths
|
||||
upload_folder = os.getcwd() + '\\tmp'
|
||||
@ -219,60 +220,63 @@ thumb_folder = os.path.join(upload_folder, media_guid)
|
||||
thumb_path = os.path.join(thumb_folder, "thumb.jpg")
|
||||
|
||||
if event == 'stop':
|
||||
if os.path.exists(thumb_folder):
|
||||
logger.debug("Removing Directory: " + thumb_folder)
|
||||
shutil.rmtree(thumb_folder)
|
||||
if os.path.exists(thumb_folder):
|
||||
logger.debug("Removing Directory: " + thumb_folder)
|
||||
shutil.rmtree(thumb_folder)
|
||||
|
||||
pifx.activate_scene(default_pause_uuid)
|
||||
exit()
|
||||
pifx.activate_scene(default_pause_uuid)
|
||||
exit()
|
||||
|
||||
if event == 'pause':
|
||||
pifx.activate_scene(default_pause_uuid)
|
||||
exit()
|
||||
pifx.activate_scene(default_pause_uuid)
|
||||
exit()
|
||||
|
||||
if event == 'play' or event == "resume":
|
||||
|
||||
# If the file already exists then we don't need to re-upload the image
|
||||
if not os.path.exists(thumb_folder):
|
||||
try:
|
||||
logger.debug("Making Directory: " + thumb_folder)
|
||||
os.makedirs(thumb_folder)
|
||||
urllib.urlretrieve(poster_url, thumb_path)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.info("No file found in request")
|
||||
pifx.activate_scene(default_play_uuid)
|
||||
exit()
|
||||
# If the file already exists then we don't need to re-upload the image
|
||||
if not os.path.exists(thumb_folder):
|
||||
try:
|
||||
logger.debug("Making Directory: " + thumb_folder)
|
||||
os.makedirs(thumb_folder)
|
||||
urllib.urlretrieve(poster_url, thumb_path)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.info("No file found in request")
|
||||
pifx.activate_scene(default_play_uuid)
|
||||
exit()
|
||||
|
||||
# Determine Color Palette for Lights
|
||||
color_thief = ColorThief(thumb_path)
|
||||
palette = color_thief.get_palette(color_count=num_colors, quality=color_quality)
|
||||
logger.debug("Color Palette: " + palette.__str__())
|
||||
color_thief = ColorThief(thumb_path)
|
||||
if num_colors >= 2:
|
||||
palette = color_thief.get_palette(color_count=num_colors, quality=color_quality)
|
||||
else:
|
||||
palette = [color_thief.get_color(quality=color_quality)]
|
||||
logger.debug("Color Palette: " + palette.__str__())
|
||||
|
||||
# Set Color Palette
|
||||
pifx.set_state(selector='all', power="off")
|
||||
for index in range(len(light_groups)):
|
||||
try:
|
||||
color = palette[index]
|
||||
light_group = light_groups[index]
|
||||
pifx.set_state(selector='all', power="off")
|
||||
for index in range(len(light_groups)):
|
||||
try:
|
||||
color = palette[index]
|
||||
light_group = light_groups[index]
|
||||
|
||||
logger.debug(light_group)
|
||||
logger.debug(color)
|
||||
logger.debug(light_group)
|
||||
logger.debug(color)
|
||||
|
||||
color_rgb = ', '.join(str(c) for c in color)
|
||||
color_rgb = "rgb:" + color_rgb
|
||||
color_rgb = color_rgb.replace(" ", "")
|
||||
color_rgb = ', '.join(str(c) for c in color)
|
||||
color_rgb = "rgb:" + color_rgb
|
||||
color_rgb = color_rgb.replace(" ", "")
|
||||
|
||||
for light_id in light_group:
|
||||
if lights_use_name:
|
||||
selector = "label:" + light_id
|
||||
else:
|
||||
selector = light_id
|
||||
for light_id in light_group:
|
||||
if lights_use_name:
|
||||
selector = "label:" + light_id
|
||||
else:
|
||||
selector = light_id
|
||||
|
||||
logger.debug("Setting light: " + selector + " to color: " + color_rgb)
|
||||
pifx.set_state(selector=selector, power="on", color=color_rgb, brightness=brightness, duration=duration)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.debug("Setting light: " + selector + " to color: " + color_rgb)
|
||||
pifx.set_state(selector=selector, power="on", color=color_rgb, brightness=brightness, duration=duration)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
exit()
|
||||
|
@ -1,11 +1,16 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
https://gist.github.com/blacktwin/4ccb79c7d01a95176b8e88bf4890cd2b
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
import random
|
||||
import re
|
||||
|
||||
|
||||
baseurl = 'http://localhost:32400'
|
||||
token = 'xxxxx'
|
||||
plex = PlexServer(baseurl, token)
|
||||
@ -44,8 +49,11 @@ def sylco(word):
|
||||
if word[-2:] == "es" or word[-2:] == "ed":
|
||||
doubleAndtripple_1 = len(re.findall(r'[eaoui][eaoui]', word))
|
||||
if doubleAndtripple_1 > 1 or len(re.findall(r'[eaoui][^eaoui]', word)) > 1:
|
||||
if word[-3:] == "ted" or word[-3:] == "tes" or word[-3:] == "ses" or word[-3:] == "ied" or word[
|
||||
-3:] == "ies":
|
||||
if word[-3:] == "ted" or \
|
||||
word[-3:] == "tes" or \
|
||||
word[-3:] == "ses" or \
|
||||
word[-3:] == "ied" or \
|
||||
word[-3:] == "ies":
|
||||
pass
|
||||
else:
|
||||
disc += 1
|
||||
@ -140,8 +148,7 @@ def sylco(word):
|
||||
if word in exception_add:
|
||||
syls += 1
|
||||
|
||||
|
||||
# calculate the output
|
||||
# calculate the output
|
||||
return numVowels - disc + syls
|
||||
|
||||
|
||||
@ -190,13 +197,13 @@ for x in LIBRARIES_LST:
|
||||
|
||||
m_lst = hi_build(sections_lst, 5) + hi_build(sections_lst, 7) + hi_build(sections_lst, 5)
|
||||
# to see word and syllable count uncomment below print.
|
||||
#print(m_lst)
|
||||
# print(m_lst)
|
||||
|
||||
stanz1 = ' '.join(m_lst[0].keys())
|
||||
stanz2 = ' '.join(m_lst[1].keys())
|
||||
stanz3 = ' '.join(m_lst[2].keys())
|
||||
|
||||
lines = stanz1,stanz2,stanz3
|
||||
lines = stanz1, stanz2, stanz3
|
||||
lines = '\n'.join(lines)
|
||||
print('')
|
||||
print(lines.lower())
|
||||
|
@ -1,4 +1,7 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
If server admin stream is experiencing buffering and there are concurrent transcode streams from
|
||||
another user, kill concurrent transcode stream that has the lowest percent complete. Message in
|
||||
kill stream will list why it was killed ('Server Admin's stream take priority and this user has X
|
||||
@ -11,15 +14,20 @@ Tautulli > Settings > Notification Agents > Scripts > Bell icon:
|
||||
Tautulli > Settings > Notification Agents > Scripts > Gear icon:
|
||||
Buffer Warnings: kill_else_if_buffering.py
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import str
|
||||
from past.utils import old_div
|
||||
import requests
|
||||
from operator import itemgetter
|
||||
import unicodedata
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
PLEX_TOKEN = 'xxxx'
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
|
||||
@ -27,8 +35,8 @@ DEFAULT_REASON = 'Server Admin\'s stream takes priority and {user}(you) has {x}
|
||||
' {user}\'s stream of {video} is {time}% complete. Should be finished in {comp} minutes. ' \
|
||||
'Try again then.'
|
||||
|
||||
ADMIN_USER = ('Admin') # additional usernames can be added ('Admin', 'user2')
|
||||
##
|
||||
ADMIN_USER = ('Admin') # Additional usernames can be added ('Admin', 'user2')
|
||||
# ##
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
@ -40,9 +48,11 @@ def kill_session(sess_key, message):
|
||||
# Check for users stream
|
||||
username = session.usernames[0]
|
||||
if session.sessionKey == sess_key:
|
||||
title = (session.grandparentTitle + ' - ' if session.type == 'episode' else '') + session.title
|
||||
print('{user} is watching {title} and they might be asleep.'.format(user=username, title=title))
|
||||
title = str(session.grandparentTitle + ' - ' if session.type == 'episode' else '') + session.title
|
||||
title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').translate(None, "'")
|
||||
session.stop(reason=message)
|
||||
print('Terminated {user}\'s stream of {title} to prioritize admin stream.'.format(user=username,
|
||||
title=title))
|
||||
|
||||
|
||||
def add_to_dictlist(d, key, val):
|
||||
@ -56,30 +66,32 @@ def main():
|
||||
user_dict = {}
|
||||
|
||||
for session in plex.sessions():
|
||||
trans_dec = session.transcodeSessions[0].videoDecision
|
||||
username = session.usernames[0]
|
||||
if trans_dec == 'transcode' and username not in ADMIN_USER:
|
||||
sess_key = session.sessionKey
|
||||
percent_comp = int((float(session.viewOffset) / float(session.duration)) * 100)
|
||||
time_to_comp = int(int(session.duration) - int(session.viewOffset)) / 1000 / 60
|
||||
title = (session.grandparentTitle + ' - ' if session.type == 'episode' else '') + session.title
|
||||
title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').translate(None, "'")
|
||||
add_to_dictlist(user_dict, username, [sess_key, percent_comp, title, username, time_to_comp])
|
||||
if session.transcodeSessions:
|
||||
trans_dec = session.transcodeSessions[0].videoDecision
|
||||
username = session.usernames[0]
|
||||
if trans_dec == 'transcode' and username not in ADMIN_USER:
|
||||
sess_key = session.sessionKey
|
||||
percent_comp = int((float(session.viewOffset) / float(session.duration)) * 100)
|
||||
time_to_comp = old_div(old_div(int(int(session.duration) - int(session.viewOffset)), 1000), 60)
|
||||
title = str(session.grandparentTitle + ' - ' if session.type == 'episode' else '') + session.title
|
||||
title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').translate(None, "'")
|
||||
add_to_dictlist(user_dict, username, [sess_key, percent_comp, title, username, time_to_comp])
|
||||
|
||||
# Remove users with only 1 stream. Targeting users with multiple concurrent streams
|
||||
filtered_dict = {key: value for key, value in user_dict.items()
|
||||
if len(value) is not 1}
|
||||
if len(value) != 1}
|
||||
|
||||
# Find who to kill and who will be finishing first.
|
||||
for users in filtered_dict.values():
|
||||
to_kill = min(users, key=itemgetter(1))
|
||||
to_finish = max(users, key=itemgetter(1))
|
||||
if filtered_dict:
|
||||
for users in filtered_dict.values():
|
||||
to_kill = min(users, key=itemgetter(1))
|
||||
to_finish = max(users, key=itemgetter(1))
|
||||
|
||||
MESSAGE = DEFAULT_REASON.format(user=to_finish[3], x=len(filtered_dict.values()[0]),
|
||||
video=to_finish[2], time=to_finish[1], comp=to_finish[4])
|
||||
MESSAGE = DEFAULT_REASON.format(user=to_finish[3], x=len(filtered_dict.values()[0]),
|
||||
video=to_finish[2], time=to_finish[1], comp=to_finish[4])
|
||||
|
||||
print(MESSAGE)
|
||||
kill_session(to_kill[0], MESSAGE)
|
||||
print(MESSAGE)
|
||||
kill_session(to_kill[0], MESSAGE)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -1,14 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Use conditions to kill a stream
|
||||
Author: Blacktwin, Arcanemagus, samwiseg00
|
||||
Requires: requests
|
||||
Author: Blacktwin, Arcanemagus, Samwiseg0, JonnyWong16, DirtyCajunRice
|
||||
|
||||
Adding the script to Tautulli:
|
||||
Taultulli > Settings > Notification Agents > Add a new notification agent >
|
||||
Tautulli > Settings > Notification Agents > Add a new notification agent >
|
||||
Script
|
||||
|
||||
Configuration:
|
||||
Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Tautulli > Settings > Notification Agents > New Script > Configuration:
|
||||
|
||||
Script Folder: /path/to/your/scripts
|
||||
Script File: ./kill_stream.py (Should be selectable in a dropdown list)
|
||||
@ -17,233 +19,547 @@ Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Save
|
||||
|
||||
Triggers:
|
||||
Taultulli > Settings > Notification Agents > New Script > Triggers:
|
||||
Tautulli > Settings > Notification Agents > New Script > Triggers:
|
||||
|
||||
Check: Playback Start and/or Playback Pause
|
||||
Save
|
||||
|
||||
Conditions:
|
||||
Taultulli > Settings > Notification Agents > New Script > Conditions:
|
||||
Tautulli > Settings > Notification Agents > New Script > Conditions:
|
||||
|
||||
Set Conditions: [{condition} | {operator} | {value} ]
|
||||
Save
|
||||
|
||||
Script Arguments:
|
||||
Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
Tautulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
Select: Playback Start, Playback Pause
|
||||
Arguments: --jbop SELECTOR --userId {user_id} --username {username}
|
||||
--sessionId {session_id} --notify notifierID
|
||||
--interval 30 --limit 1200
|
||||
--richMessage RICH_TYPE --serverName {server_name}
|
||||
--plexUrl {plex_url} --posterUrl {poster_url}
|
||||
--richColor '#E5A00D'
|
||||
--killMessage 'Your message here.'
|
||||
|
||||
Save
|
||||
Close
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from builtins import object
|
||||
from builtins import str
|
||||
import os
|
||||
from time import sleep
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
TAUTULLI_PUBLIC_URL = ''
|
||||
TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL)
|
||||
TAUTULLI_PUBLIC_URL = os.getenv('TAUTULLI_PUBLIC_URL', TAUTULLI_PUBLIC_URL)
|
||||
TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
|
||||
TAUTULLI_ENCODING = os.getenv('TAUTULLI_ENCODING', 'UTF-8')
|
||||
VERIFY_SSL = False
|
||||
|
||||
if TAUTULLI_PUBLIC_URL != '/':
|
||||
# Check to see if there is a public URL set in Tautulli
|
||||
TAUTULLI_LINK = TAUTULLI_PUBLIC_URL
|
||||
else:
|
||||
TAUTULLI_LINK = TAUTULLI_URL
|
||||
|
||||
SUBJECT_TEXT = "Tautulli has killed a stream."
|
||||
BODY_TEXT = "Killed session ID '{id}'. Reason: {message}"
|
||||
BODY_TEXT_USER = "Killed {user}'s stream. Reason: {message}."
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
SELECTOR = ['stream', 'allStreams', 'paused']
|
||||
|
||||
RICH_TYPE = ['discord', 'slack']
|
||||
|
||||
def send_notification(subject_text, body_text, notifier_id):
|
||||
"""Send a notification through Tautulli
|
||||
TAUTULLI_ICON = 'https://github.com/Tautulli/Tautulli/raw/master/data/interfaces/default/images/logo-circle.png'
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subject_text : str
|
||||
The text to use for the subject line of the message.
|
||||
body_text : str
|
||||
The text to use for the body of the notification.
|
||||
notifier_id : int
|
||||
Tautulli Notification Agent ID to send the notification to.
|
||||
"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'notify',
|
||||
'notifier_id': notifier_id,
|
||||
'subject': subject_text,
|
||||
'body': body_text}
|
||||
|
||||
def utc_now_iso():
|
||||
"""Get current time in ISO format"""
|
||||
utcnow = datetime.utcnow()
|
||||
|
||||
return utcnow.isoformat()
|
||||
|
||||
|
||||
def hex_to_int(value):
|
||||
"""Convert hex value to integer"""
|
||||
try:
|
||||
r = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
|
||||
if response['response']['result'] == 'success':
|
||||
sys.stdout.write("Successfully sent Tautulli notification.\n")
|
||||
else:
|
||||
raise Exception(response['response']['message'])
|
||||
except Exception as e:
|
||||
sys.stderr.write(
|
||||
"Tautulli API 'notify' request failed: {0}.\n".format(e))
|
||||
return None
|
||||
return int(value, 16)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
def get_activity():
|
||||
"""Get the current activity on the PMS.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
The current active sessions on the Plex server.
|
||||
"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_activity'}
|
||||
|
||||
try:
|
||||
req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = req.json()
|
||||
|
||||
res_data = response['response']['data']['sessions']
|
||||
return res_data
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write(
|
||||
"Tautulli API 'get_activity' request failed: {0}.\n".format(e))
|
||||
return []
|
||||
def arg_decoding(arg):
|
||||
"""Decode args, encode UTF-8"""
|
||||
if sys.version_info[0] < 3:
|
||||
return arg.decode(TAUTULLI_ENCODING).encode('UTF-8')
|
||||
else:
|
||||
return arg
|
||||
|
||||
|
||||
def get_user_session_ids(user_id):
|
||||
"""Get current session IDs for a specific user.
|
||||
def debug_dump_vars():
|
||||
"""Dump parameters for debug"""
|
||||
print('Tautulli URL - ' + TAUTULLI_URL)
|
||||
print('Tautulli Public URL - ' + TAUTULLI_PUBLIC_URL)
|
||||
print('Verify SSL - ' + str(VERIFY_SSL))
|
||||
print('Tautulli API key - ' + TAUTULLI_APIKEY[-4:]
|
||||
.rjust(len(TAUTULLI_APIKEY), "x"))
|
||||
|
||||
|
||||
def get_all_streams(tautulli, user_id=None):
|
||||
"""Get a list of all current streams.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_id : int
|
||||
The ID of the user to grab sessions for.
|
||||
|
||||
tautulli : obj
|
||||
Tautulli object.
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
The active session IDs for the specific user ID.
|
||||
|
||||
objects
|
||||
The of stream objects.
|
||||
"""
|
||||
sessions = get_activity()
|
||||
user_streams = [s['session_id']
|
||||
for s in sessions if s['user_id'] == user_id]
|
||||
return user_streams
|
||||
sessions = tautulli.get_activity()['sessions']
|
||||
|
||||
if user_id:
|
||||
streams = [Stream(session=s) for s in sessions if s['user_id'] == user_id]
|
||||
else:
|
||||
streams = [Stream(session=s) for s in sessions]
|
||||
|
||||
return streams
|
||||
|
||||
|
||||
def terminate_session(session_id, message, notifier=None, username=None):
|
||||
"""Stop a streaming session.
|
||||
def notify(all_opts, message, kill_type=None, stream=None, tautulli=None):
|
||||
"""Decides which notifier type to use"""
|
||||
if all_opts.notify and all_opts.richMessage:
|
||||
rich_notify(all_opts.notify, all_opts.richMessage, all_opts.richColor, kill_type,
|
||||
all_opts.serverName, all_opts.plexUrl, all_opts.posterUrl, message, stream, tautulli)
|
||||
elif all_opts.notify:
|
||||
basic_notify(all_opts.notify, all_opts.sessionId, all_opts.username, message, stream, tautulli)
|
||||
|
||||
|
||||
def rich_notify(notifier_id, rich_type, color=None, kill_type=None, server_name=None,
|
||||
plex_url=None, poster_url=None, message=None, stream=None, tautulli=None):
|
||||
"""Decides which rich notifier type to use. Set default values for empty variables
|
||||
|
||||
Parameters
|
||||
----------
|
||||
session_id : str
|
||||
The session ID of the stream to terminate.
|
||||
notifier_id : int
|
||||
The ID of the user to grab sessions for.
|
||||
rich_type : str
|
||||
Contains 'discord' or 'slack'.
|
||||
color : Union[int, str]
|
||||
Hex string or integer representation of color.
|
||||
kill_type : str
|
||||
The kill type used.
|
||||
server_name : str
|
||||
The name of the plex server.
|
||||
plex_url : str
|
||||
Plex media URL.
|
||||
poster_url : str
|
||||
The media poster URL.
|
||||
message : str
|
||||
The message to display to the user when terminating a stream.
|
||||
notifier : int
|
||||
Notification agent ID to send a message to (the default is None).
|
||||
username : str
|
||||
The username for the terminated session (the default is None).
|
||||
Message sent to the client.
|
||||
stream : obj
|
||||
Stream object.
|
||||
tautulli : obj
|
||||
Tautulli object.
|
||||
"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'terminate_session',
|
||||
'session_id': session_id,
|
||||
'message': message}
|
||||
notification = Notification(notifier_id, None, None, tautulli, stream)
|
||||
# Initialize Variables
|
||||
title = ''
|
||||
footer = ''
|
||||
# Set a default server_name if none is provided
|
||||
if server_name is None:
|
||||
server_name = 'Plex Server'
|
||||
|
||||
try:
|
||||
req = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = req.json()
|
||||
# Set a default color if none is provided
|
||||
if color is None:
|
||||
color = '#E5A00D'
|
||||
|
||||
if response['response']['result'] == 'success':
|
||||
sys.stdout.write(
|
||||
"Successfully killed Plex session: {0}.\n".format(session_id))
|
||||
if notifier:
|
||||
if username:
|
||||
body = BODY_TEXT_USER.format(user=username,
|
||||
message=message)
|
||||
else:
|
||||
body = BODY_TEXT.format(id=session_id, message=message)
|
||||
send_notification(SUBJECT_TEXT, body, notifier)
|
||||
else:
|
||||
raise Exception(response['response']['message'])
|
||||
except Exception as e:
|
||||
sys.stderr.write(
|
||||
"Tautulli API 'terminate_session' request failed: {0}.".format(e))
|
||||
return None
|
||||
# Set a default plexUrl if none is provided
|
||||
if plex_url is None:
|
||||
plex_url = 'https://app.plex.tv'
|
||||
|
||||
# Set a default posterUrl if none is provided
|
||||
if poster_url is None:
|
||||
poster_url = TAUTULLI_ICON
|
||||
|
||||
# Set a default message if none is provided
|
||||
if message is None:
|
||||
message = 'The server owner has ended the stream.'
|
||||
|
||||
if kill_type == 'Stream':
|
||||
title = "Killed {}'s stream.".format(stream.friendly_name)
|
||||
footer = '{} | Kill {}'.format(server_name, kill_type)
|
||||
|
||||
elif kill_type == 'Paused':
|
||||
title = "Killed {}'s paused stream.".format(stream.friendly_name)
|
||||
footer = '{} | Kill {}'.format(server_name, kill_type)
|
||||
|
||||
elif kill_type == 'All Streams':
|
||||
title = "Killed {}'s stream.".format(stream.friendly_name)
|
||||
footer = '{} | Kill {}'.format(server_name, kill_type)
|
||||
poster_url = TAUTULLI_ICON
|
||||
plex_url = 'https://app.plex.tv'
|
||||
|
||||
if rich_type == 'discord':
|
||||
color = hex_to_int(color.lstrip('#'))
|
||||
notification.send_discord(title, color, poster_url, plex_url, message, footer)
|
||||
|
||||
elif rich_type == 'slack':
|
||||
notification.send_slack(title, color, poster_url, plex_url, message, footer)
|
||||
|
||||
|
||||
def terminate_long_pause(session_id, message, limit, interval, notify=None):
|
||||
"""Kills the session if it is paused for longer than <limit> seconds.
|
||||
def basic_notify(notifier_id, session_id, username=None, message=None, stream=None, tautulli=None):
|
||||
"""Basic notifier"""
|
||||
notification = Notification(notifier_id, SUBJECT_TEXT, BODY_TEXT, tautulli, stream)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
session_id : str
|
||||
The session id of the session to monitor.
|
||||
message : str
|
||||
The message to use if the stream is terminated.
|
||||
limit : int
|
||||
The number of seconds the session is allowed to remain paused before it
|
||||
is terminated.
|
||||
interval : int
|
||||
The amount of time to wait between checks of the session state.
|
||||
notify : int
|
||||
Tautulli Notification Agent ID to send a notification to on killing a
|
||||
stream.
|
||||
"""
|
||||
start = datetime.now()
|
||||
checked_time = 0
|
||||
# Continue checking 2 intervals past the allowed limit in order to
|
||||
# account for system variances.
|
||||
check_limit = limit + (interval * 2)
|
||||
if username:
|
||||
body = BODY_TEXT_USER.format(user=username,
|
||||
message=message)
|
||||
else:
|
||||
body = BODY_TEXT.format(id=session_id, message=message)
|
||||
notification.send(SUBJECT_TEXT, body)
|
||||
|
||||
while checked_time < check_limit:
|
||||
sessions = get_activity()
|
||||
found_session = False
|
||||
|
||||
for session in sessions:
|
||||
if session['session_id'] == session_id:
|
||||
found_session = True
|
||||
state = session['state']
|
||||
now = datetime.now()
|
||||
checked_time = (now - start).total_seconds()
|
||||
class Tautulli(object):
|
||||
def __init__(self, url, apikey, verify_ssl=False, debug=None):
|
||||
self.url = url
|
||||
self.apikey = apikey
|
||||
self.debug = debug
|
||||
|
||||
if state == 'paused':
|
||||
if checked_time >= limit:
|
||||
terminate_session(session_id, message, notify)
|
||||
return
|
||||
else:
|
||||
sleep(interval)
|
||||
elif state == 'playing' or state == 'buffering':
|
||||
sys.stdout.write(
|
||||
"Session '{}' has resumed, ".format(session_id) +
|
||||
"stopping monitoring.\n")
|
||||
return
|
||||
if not found_session:
|
||||
sys.stdout.write(
|
||||
"Session '{}' is no longer active ".format(session_id) +
|
||||
"on the server, stopping monitoring.\n")
|
||||
self.session = Session()
|
||||
self.adapters = HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session.mount('http://', self.adapters)
|
||||
self.session.mount('https://', self.adapters)
|
||||
|
||||
# Ignore verifying the SSL certificate
|
||||
if verify_ssl is False:
|
||||
self.session.verify = False
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
def _call_api(self, cmd, payload, method='GET'):
|
||||
payload['cmd'] = cmd
|
||||
payload['apikey'] = self.apikey
|
||||
|
||||
try:
|
||||
response = self.session.request(method, self.url + '/api/v2', params=payload)
|
||||
except RequestException as e:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
|
||||
if self.debug:
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
except ValueError:
|
||||
print(
|
||||
"Failed to parse json response for Tautulli API cmd '{}': {}"
|
||||
.format(cmd, response.content))
|
||||
return
|
||||
|
||||
def arg_decoding(arg):
|
||||
return arg.decode(TAUTULLI_ENCODING).encode('UTF-8')
|
||||
if response_json['response']['result'] == 'success':
|
||||
if self.debug:
|
||||
print("Successfully called Tautulli API cmd '{}'".format(cmd))
|
||||
return response_json['response']['data']
|
||||
else:
|
||||
error_msg = response_json['response']['message']
|
||||
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
|
||||
return
|
||||
|
||||
def get_activity(self, session_key=None, session_id=None):
|
||||
"""Call Tautulli's get_activity api endpoint"""
|
||||
payload = {}
|
||||
|
||||
if session_key:
|
||||
payload['session_key'] = session_key
|
||||
elif session_id:
|
||||
payload['session_id'] = session_id
|
||||
|
||||
return self._call_api('get_activity', payload)
|
||||
|
||||
def notify(self, notifier_id, subject, body):
|
||||
"""Call Tautulli's notify api endpoint"""
|
||||
payload = {'notifier_id': notifier_id,
|
||||
'subject': subject,
|
||||
'body': body}
|
||||
|
||||
return self._call_api('notify', payload)
|
||||
|
||||
def terminate_session(self, session_key=None, session_id=None, message=''):
|
||||
"""Call Tautulli's terminate_session api endpoint"""
|
||||
payload = {}
|
||||
|
||||
if session_key:
|
||||
payload['session_key'] = session_key
|
||||
elif session_id:
|
||||
payload['session_id'] = session_id
|
||||
|
||||
if message:
|
||||
payload['message'] = message
|
||||
|
||||
return self._call_api('terminate_session', payload)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
def __init__(self, session_id=None, user_id=None, username=None, tautulli=None, session=None):
|
||||
self.state = None
|
||||
self.ip_address = None
|
||||
self.session_id = session_id
|
||||
self.user_id = user_id
|
||||
self.username = username
|
||||
self.session_exists = False
|
||||
self.tautulli = tautulli
|
||||
|
||||
if session is not None:
|
||||
self._set_stream_attributes(session)
|
||||
|
||||
def _set_stream_attributes(self, session):
|
||||
for k, v in session.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def get_all_stream_info(self):
|
||||
"""Get all stream info from Tautulli."""
|
||||
session = self.tautulli.get_activity(session_id=self.session_id)
|
||||
if session:
|
||||
self._set_stream_attributes(session)
|
||||
self.session_exists = True
|
||||
else:
|
||||
self.session_exists = False
|
||||
|
||||
def terminate(self, message=''):
|
||||
"""Calls Tautulli to terminate the session.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message : str
|
||||
The message to use if the stream is terminated.
|
||||
"""
|
||||
self.tautulli.terminate_session(session_id=self.session_id, message=message)
|
||||
|
||||
def terminate_long_pause(self, message, limit, interval):
|
||||
"""Kills the session if it is paused for longer than <limit> seconds.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message : str
|
||||
The message to use if the stream is terminated.
|
||||
limit : int
|
||||
The number of seconds the session is allowed to remain paused before it
|
||||
is terminated.
|
||||
interval : int
|
||||
The amount of time to wait between checks of the session state.
|
||||
"""
|
||||
start = datetime.now()
|
||||
checked_time = 0
|
||||
# Continue checking 2 intervals past the allowed limit in order to
|
||||
# account for system variances.
|
||||
check_limit = limit + (interval * 2)
|
||||
|
||||
while checked_time < check_limit:
|
||||
self.get_all_stream_info()
|
||||
|
||||
if self.session_exists is False:
|
||||
sys.stdout.write(
|
||||
"Session '{}' from user '{}' is no longer active "
|
||||
.format(self.session_id, self.username) +
|
||||
"on the server, stopping monitoring.\n")
|
||||
return False
|
||||
|
||||
now = datetime.now()
|
||||
checked_time = (now - start).total_seconds()
|
||||
|
||||
if self.state == 'paused':
|
||||
if checked_time >= limit:
|
||||
self.terminate(message)
|
||||
sys.stdout.write(
|
||||
"Session '{}' from user '{}' has been killed.\n"
|
||||
.format(self.session_id, self.username))
|
||||
return True
|
||||
else:
|
||||
time.sleep(interval)
|
||||
|
||||
elif self.state == 'playing' or self.state == 'buffering':
|
||||
sys.stdout.write(
|
||||
"Session '{}' from user '{}' has been resumed, "
|
||||
.format(self.session_id, self.username) +
|
||||
"stopping monitoring.\n")
|
||||
return False
|
||||
|
||||
|
||||
class Notification(object):
|
||||
def __init__(self, notifier_id, subject, body, tautulli, stream):
|
||||
self.notifier_id = notifier_id
|
||||
self.subject = subject
|
||||
self.body = body
|
||||
|
||||
self.tautulli = tautulli
|
||||
self.stream = stream
|
||||
|
||||
def send(self, subject='', body=''):
|
||||
"""Send to Tautulli notifier.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subject : str
|
||||
Subject of the message.
|
||||
body : str
|
||||
Body of the message.
|
||||
"""
|
||||
subject = subject or self.subject
|
||||
body = body or self.body
|
||||
self.tautulli.notify(notifier_id=self.notifier_id, subject=subject, body=body)
|
||||
|
||||
def send_discord(self, title, color, poster_url, plex_url, message, footer):
|
||||
"""Build the Discord message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title of the message.
|
||||
color : int
|
||||
The color of the message
|
||||
poster_url : str
|
||||
The media poster URL.
|
||||
plex_url : str
|
||||
Plex media URL.
|
||||
message : str
|
||||
Message sent to the player.
|
||||
footer : str
|
||||
Footer of the message.
|
||||
"""
|
||||
discord_message = {
|
||||
"embeds": [
|
||||
{
|
||||
"author": {
|
||||
"icon_url": TAUTULLI_ICON,
|
||||
"name": "Tautulli",
|
||||
"url": TAUTULLI_LINK.rstrip('/')
|
||||
},
|
||||
"color": color,
|
||||
"fields": [
|
||||
{
|
||||
"inline": True,
|
||||
"name": "User",
|
||||
"value": self.stream.friendly_name
|
||||
},
|
||||
{
|
||||
"inline": True,
|
||||
"name": "Session Key",
|
||||
"value": self.stream.session_key
|
||||
},
|
||||
{
|
||||
"inline": True,
|
||||
"name": "Watching",
|
||||
"value": self.stream.full_title
|
||||
},
|
||||
{
|
||||
"inline": False,
|
||||
"name": "Message Sent",
|
||||
"value": message
|
||||
}
|
||||
],
|
||||
"thumbnail": {
|
||||
"url": poster_url
|
||||
},
|
||||
"title": title,
|
||||
"timestamp": utc_now_iso(),
|
||||
"url": plex_url,
|
||||
"footer": {
|
||||
"text": footer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
discord_message = json.dumps(discord_message, sort_keys=True,
|
||||
separators=(',', ': '))
|
||||
self.send(body=discord_message)
|
||||
|
||||
def send_slack(self, title, color, poster_url, plex_url, message, footer):
|
||||
"""Build the Slack message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title of the message.
|
||||
color : int
|
||||
The color of the message
|
||||
poster_url : str
|
||||
The media poster URL.
|
||||
plex_url : str
|
||||
Plex media URL.
|
||||
message : str
|
||||
Message sent to the player.
|
||||
footer : str
|
||||
Footer of the message.
|
||||
"""
|
||||
slack_message = {
|
||||
"attachments": [
|
||||
{
|
||||
"title": title,
|
||||
"title_link": plex_url,
|
||||
"author_icon": TAUTULLI_ICON,
|
||||
"author_name": "Tautulli",
|
||||
"author_link": TAUTULLI_LINK.rstrip('/'),
|
||||
"color": color,
|
||||
"fields": [
|
||||
{
|
||||
"title": "User",
|
||||
"value": self.stream.friendly_name,
|
||||
"short": True
|
||||
},
|
||||
{
|
||||
"title": "Session Key",
|
||||
"value": self.stream.session_key,
|
||||
"short": True
|
||||
},
|
||||
{
|
||||
"title": "Watching",
|
||||
"value": self.stream.full_title,
|
||||
"short": True
|
||||
},
|
||||
{
|
||||
"title": "Message Sent",
|
||||
"value": message,
|
||||
"short": False
|
||||
}
|
||||
],
|
||||
"thumb_url": poster_url,
|
||||
"footer": footer,
|
||||
"ts": time.time()
|
||||
}
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
slack_message = json.dumps(slack_message, sort_keys=True,
|
||||
separators=(',', ': '))
|
||||
self.send(body=slack_message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@ -255,35 +571,69 @@ if __name__ == "__main__":
|
||||
help='The unique identifier for the user.')
|
||||
parser.add_argument('--username', type=arg_decoding,
|
||||
help='The username of the person streaming.')
|
||||
parser.add_argument('--sessionId', required=True,
|
||||
parser.add_argument('--sessionId',
|
||||
help='The unique identifier for the stream.')
|
||||
parser.add_argument('--notify', type=int,
|
||||
help='Notification Agent ID number to Agent to send ' +
|
||||
'notification.')
|
||||
help='Notification Agent ID number to Agent to ' +
|
||||
'send notification.')
|
||||
parser.add_argument('--limit', type=int, default=(20 * 60), # 20 minutes
|
||||
help='The time session is allowed to remain paused.')
|
||||
parser.add_argument('--interval', type=int, default=30,
|
||||
help='The seconds between paused session checks.')
|
||||
parser.add_argument('--killMessage', nargs='+', type=arg_decoding,
|
||||
help='Message to send to user whose stream is killed.')
|
||||
parser.add_argument('--richMessage', type=arg_decoding, choices=RICH_TYPE,
|
||||
help='Rich message type selector.\nChoices: (%(choices)s)')
|
||||
parser.add_argument('--serverName', type=arg_decoding,
|
||||
help='Plex Server Name')
|
||||
parser.add_argument('--plexUrl', type=arg_decoding,
|
||||
help='URL to plex media')
|
||||
parser.add_argument('--posterUrl', type=arg_decoding,
|
||||
help='Poster URL of the media')
|
||||
parser.add_argument('--richColor', type=arg_decoding,
|
||||
help='Color of the rich message')
|
||||
parser.add_argument("--debug", action='store_true',
|
||||
help='Enable debug messages.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
if not opts.sessionId:
|
||||
if not opts.sessionId and opts.jbop != 'allStreams':
|
||||
sys.stderr.write("No sessionId provided! Is this synced content?\n")
|
||||
sys.exit(1)
|
||||
|
||||
if opts.debug:
|
||||
# Import traceback to get more detailed information
|
||||
import traceback
|
||||
# Dump the ENVs passed from tautulli
|
||||
debug_dump_vars()
|
||||
|
||||
# Create a Tautulli instance
|
||||
tautulli_server = Tautulli(TAUTULLI_URL.rstrip('/'), TAUTULLI_APIKEY, VERIFY_SSL, opts.debug)
|
||||
|
||||
# Create initial Stream object with basic info
|
||||
tautulli_stream = Stream(opts.sessionId, opts.userId, opts.username, tautulli_server)
|
||||
|
||||
# Only pull all stream info if using richMessage
|
||||
if opts.notify and opts.richMessage:
|
||||
tautulli_stream.get_all_stream_info()
|
||||
|
||||
# Set a default message if none is provided
|
||||
if opts.killMessage:
|
||||
message = ' '.join(opts.killMessage)
|
||||
kill_message = ' '.join(opts.killMessage)
|
||||
else:
|
||||
message = ''
|
||||
kill_message = 'The server owner has ended the stream.'
|
||||
|
||||
if opts.jbop == 'stream':
|
||||
terminate_session(opts.sessionId, message, opts.notify, opts.username)
|
||||
tautulli_stream.terminate(kill_message)
|
||||
notify(opts, kill_message, 'Stream', tautulli_stream, tautulli_server)
|
||||
|
||||
elif opts.jbop == 'allStreams':
|
||||
streams = get_user_session_ids(opts.userId)
|
||||
for session_id in streams:
|
||||
terminate_session(session_id, message, opts.notify, opts.username)
|
||||
all_streams = get_all_streams(tautulli_server, opts.userId)
|
||||
for a_stream in all_streams:
|
||||
tautulli_server.terminate_session(session_id=a_stream.session_id, message=kill_message)
|
||||
notify(opts, kill_message, 'All Streams', a_stream, tautulli_server)
|
||||
|
||||
elif opts.jbop == 'paused':
|
||||
terminate_long_pause(opts.sessionId, message, opts.limit,
|
||||
opts.interval, opts.notify)
|
||||
killed_stream = tautulli_stream.terminate_long_pause(kill_message, opts.limit, opts.interval)
|
||||
if killed_stream:
|
||||
notify(opts, kill_message, 'Paused', tautulli_stream, tautulli_server)
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Limiting Plex users by plays, watches, or total time from Tautulli.
|
||||
Author: Blacktwin, Arcanemagus
|
||||
@ -37,18 +40,23 @@ Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
--grandparent_rating_key {grandparent_rating_key}
|
||||
--limit plays=3 --delay 60
|
||||
--killMessage 'Your message here.'
|
||||
--today
|
||||
|
||||
Save
|
||||
Close
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import range
|
||||
import requests
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import sys
|
||||
import os
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
from plexapi.server import PlexServer
|
||||
from time import time as ttime
|
||||
from time import sleep
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
@ -63,6 +71,7 @@ TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
|
||||
TAUTULLI_ENCODING = os.getenv('TAUTULLI_ENCODING', 'UTF-8')
|
||||
|
||||
# Using CONFIG file
|
||||
# from plexapi.server import CONFIG
|
||||
# PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
# PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
# TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl', TAUTULLI_URL)
|
||||
@ -86,11 +95,11 @@ if sess.verify is False:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
lib_dict = {x.title : x.key for x in plex.library.sections()}
|
||||
lib_dict = {x.title: x.key for x in plex.library.sections()}
|
||||
|
||||
|
||||
SELECTOR = ['watch', 'plays', 'time', 'limit']
|
||||
TODAY = datetime.today().strftime('%Y-%m-%d')
|
||||
TODAY = datetime.now()
|
||||
unix_time = int(ttime())
|
||||
|
||||
|
||||
@ -126,7 +135,7 @@ def send_notification(subject_text, body_text, notifier_id):
|
||||
return None
|
||||
|
||||
|
||||
def get_activity():
|
||||
def get_activity(session_id=None):
|
||||
"""Get the current activity on the PMS.
|
||||
|
||||
Returns
|
||||
@ -136,12 +145,17 @@ def get_activity():
|
||||
"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_activity'}
|
||||
if session_id:
|
||||
payload['session_id'] = session_id
|
||||
|
||||
try:
|
||||
req = sess.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = req.json()
|
||||
|
||||
res_data = response['response']['data']['sessions']
|
||||
if session_id:
|
||||
res_data = response['response']['data']
|
||||
else:
|
||||
res_data = response['response']['data']['sessions']
|
||||
return res_data
|
||||
|
||||
except Exception as e:
|
||||
@ -160,14 +174,14 @@ def get_history(username, start_date=None, section_id=None):
|
||||
|
||||
Optional
|
||||
----------
|
||||
start_date : str "YYYY-MM-DD"
|
||||
start_date : list ["YYYY-MM-DD", ...]
|
||||
The date in history to search.
|
||||
section_id : int
|
||||
The libraries numeric identifier
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
dict
|
||||
The total number of watches, plays, or total playtime.
|
||||
"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -175,7 +189,7 @@ def get_history(username, start_date=None, section_id=None):
|
||||
'user': username}
|
||||
|
||||
if start_date:
|
||||
payload['start_date'] = TODAY
|
||||
payload['start_date'] = start_date
|
||||
|
||||
if section_id:
|
||||
payload['section_id '] = section_id
|
||||
@ -191,26 +205,6 @@ def get_history(username, start_date=None, section_id=None):
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_user_session_ids(user_id):
|
||||
"""Get current session IDs for a specific user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_id : int
|
||||
The ID of the user to grab sessions for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
The active session IDs for the specific user ID.
|
||||
|
||||
"""
|
||||
sessions = get_activity()
|
||||
user_streams = [s['session_id']
|
||||
for s in sessions if s['user_id'] == user_id]
|
||||
return user_streams
|
||||
|
||||
|
||||
def terminate_session(session_id, message, notifier=None, username=None):
|
||||
"""Stop a streaming session.
|
||||
|
||||
@ -234,7 +228,6 @@ def terminate_session(session_id, message, notifier=None, username=None):
|
||||
req = sess.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = req.json()
|
||||
|
||||
print(response)
|
||||
if response['response']['result'] == 'success':
|
||||
sys.stdout.write(
|
||||
"Successfully killed Plex session: {0}.\n".format(session_id))
|
||||
@ -257,12 +250,6 @@ def arg_decoding(arg):
|
||||
return arg.decode(TAUTULLI_ENCODING).encode('UTF-8')
|
||||
|
||||
|
||||
def unshare(user):
|
||||
print('{user} has reached their limit. Unsharing...'.format(user=user))
|
||||
plex.myPlexAccount().updateFriend(user=user, server=plex, removeSections=True, sections='2')
|
||||
print('Unshared all libraries from {user}.'.format(user=user))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Limiting Plex users by plays, watches, or total time from Tautulli.")
|
||||
@ -286,11 +273,24 @@ if __name__ == "__main__":
|
||||
parser.add_argument('--section', default=False, choices=lib_dict.keys(), metavar='',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s)')
|
||||
parser.add_argument('--days', type=int, default=0, nargs='?',
|
||||
help='Search history limit. \n'
|
||||
'Default: %(default)s day(s) (today).')
|
||||
parser.add_argument('--duration', type=int, default=0,
|
||||
help='Duration of item that triggered script agent.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
history_lst = []
|
||||
total_limit = 0
|
||||
total_jbop = 0
|
||||
duration = 0
|
||||
dates = []
|
||||
delta = timedelta(days=opts.days)
|
||||
|
||||
for i in range(delta.days + 1):
|
||||
day = TODAY + timedelta(days=-i)
|
||||
dates.append(day.strftime('%Y-%m-%d'))
|
||||
|
||||
if opts.limit:
|
||||
limit = dict(opts.limit)
|
||||
@ -309,47 +309,83 @@ if __name__ == "__main__":
|
||||
sys.stderr.write("No sessionId provided! Is this synced content?\n")
|
||||
sys.exit(1)
|
||||
|
||||
if opts.duration:
|
||||
# If duration is used convert to seconds from minutes
|
||||
duration = opts.duration * 60
|
||||
|
||||
if opts.killMessage:
|
||||
message = ' '.join(opts.killMessage)
|
||||
else:
|
||||
message = ''
|
||||
|
||||
if opts.section:
|
||||
section_id = lib_dict[opts.section]
|
||||
history = get_history(username=opts.username, section_id=section_id)
|
||||
else:
|
||||
history = get_history(username=opts.username)
|
||||
|
||||
for date in dates:
|
||||
if opts.section:
|
||||
section_id = lib_dict[opts.section]
|
||||
history = get_history(username=opts.username, section_id=section_id, start_date=date)
|
||||
else:
|
||||
history = get_history(username=opts.username, start_date=date)
|
||||
history_lst.append(history)
|
||||
|
||||
if opts.jbop == 'watch':
|
||||
total_jbop = sum([data['watched_status'] for data in history['data']])
|
||||
total_jbop = sum([data['watched_status'] for history in history_lst for data in history['data']])
|
||||
if opts.jbop == 'time':
|
||||
total_jbop = sum([data['duration'] for data in history['data']])
|
||||
total_jbop = sum([data['duration'] for history in history_lst for data in history['data']])
|
||||
if opts.jbop == 'plays':
|
||||
total_jbop = history['recordsFiltered']
|
||||
total_jbop = sum([history['recordsFiltered'] for history in history_lst])
|
||||
|
||||
if total_jbop:
|
||||
if total_jbop > total_limit:
|
||||
print('Total {} ({}) is greater than limit ({}).'
|
||||
.format(opts.jbop, total_jbop, total_limit))
|
||||
terminate_session(opts.sessionId, message, opts.notify, opts.username)
|
||||
else:
|
||||
print('Total {} ({}) is less than limit ({}).'
|
||||
.format(opts.jbop, total_jbop, total_limit))
|
||||
elif (duration + total_jbop) > total_limit:
|
||||
interval = 60
|
||||
start = 0
|
||||
while (start + total_jbop) < total_limit:
|
||||
if get_activity(opts.sessionId):
|
||||
sleep(interval)
|
||||
start += interval
|
||||
else:
|
||||
print('Session; {} has been dropped. Stopping monitoring of stream.'.format(opts.sessionId))
|
||||
exit()
|
||||
|
||||
print('Total {} ({} + current item duration {}) is greater than limit ({}).'
|
||||
.format(opts.jbop, total_jbop, duration, total_limit))
|
||||
terminate_session(opts.sessionId, message, opts.notify, opts.username)
|
||||
else:
|
||||
if duration:
|
||||
print('Total {} ({} + current item duration {}) is less than limit ({}).'
|
||||
.format(opts.jbop, total_jbop, duration, total_limit))
|
||||
else:
|
||||
print('Total {} ({}) is less than limit ({}).'
|
||||
.format(opts.jbop, total_jbop, total_limit))
|
||||
# todo-me need more flexibility for pulling history
|
||||
# limit work requires gp_rating_key only? Needs more options.
|
||||
if opts.jbop == 'limit' and opts.grandparent_rating_key:
|
||||
history = get_history(username=opts.username, start_date=True)
|
||||
message = LIMIT_MESSAGE.format(delay=opts.delay)
|
||||
ep_watched = [data['watched_status'] for data in history['data']
|
||||
if data['grandparent_rating_key'] == opts.grandparent_rating_key
|
||||
and data['watched_status'] == 1]
|
||||
ep_watched = []
|
||||
stopped_time = []
|
||||
for date in dates:
|
||||
history_lst.append(get_history(username=opts.username, start_date=date))
|
||||
# If message is not already defined use default message
|
||||
if not message:
|
||||
message = LIMIT_MESSAGE.format(delay=opts.delay)
|
||||
for history in history_lst:
|
||||
ep_watched += [data['watched_status'] for data in history['data']
|
||||
if data['grandparent_rating_key'] == opts.grandparent_rating_key and
|
||||
data['watched_status'] == 1]
|
||||
|
||||
stopped_time += [data['stopped'] for data in history['data']
|
||||
if data['grandparent_rating_key'] == opts.grandparent_rating_key and
|
||||
data['watched_status'] == 1]
|
||||
|
||||
# If show has no history for date range start at 0.
|
||||
if not ep_watched:
|
||||
ep_watched = 0
|
||||
else:
|
||||
ep_watched = sum(ep_watched)
|
||||
|
||||
stopped_time = [data['stopped'] for data in history['data']
|
||||
if data['grandparent_rating_key'] == opts.grandparent_rating_key
|
||||
and data['watched_status'] == 1]
|
||||
# If show episode have not been stopped (completed?) then use current time.
|
||||
# Last stopped time is needed to test against auto play timer
|
||||
if not stopped_time:
|
||||
stopped_time = unix_time
|
||||
else:
|
||||
@ -360,8 +396,9 @@ if __name__ == "__main__":
|
||||
sys.exit(1)
|
||||
|
||||
if ep_watched >= total_limit:
|
||||
print("{}'s limit is {} and has watched {} episodes of this show.".format(
|
||||
opts.username, total_limit, ep_watched))
|
||||
terminate_session(opts.sessionId, message, opts.notify, opts.username)
|
||||
else:
|
||||
print("{}'s limit is {} but has only watched {} episodes of this show today."
|
||||
.format(opts.username, total_limit, ep_watched))
|
||||
|
||||
print("{}'s limit is {} but has only watched {} episodes of this show.".format(
|
||||
opts.username, total_limit, ep_watched))
|
||||
|
@ -7,11 +7,11 @@ Killing streams is a Plex Pass only feature. So these scripts will **only** work
|
||||
### Limit user to an amount of Plays of a show during a time of day
|
||||
_For users falling asleep while autoplaying a show_ :sleeping:\
|
||||
Triggers: Playback Start
|
||||
Conditions: \[ `Current Hour` | `is` | `22 or 23 or 24 or 1` \]
|
||||
Conditions: \[ `Current Hour` | `is` | `22 or 23 or 0 or 1` \]
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop limit --username {username} --sessionId {session_id} --grandparent_rating_key {grandparent_rating_key} --limit plays=3 --delay 60 --killMessage 'You sleeping?'
|
||||
--jbop limit --username {username} --sessionId {session_id} --grandparent_rating_key {grandparent_rating_key} --limit plays=3 --delay 60 --killMessage "You sleeping?"
|
||||
```
|
||||
|
||||
### Limit user to total Plays/Watches and send a notification to agent 1
|
||||
@ -20,7 +20,16 @@ Triggers: Playback Start
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop watch --username {username} --sessionId {session_id} --limit plays=3 --notify 1 --killMessage 'You have met your limit of 3 watches.'
|
||||
--jbop watch --username {username} --sessionId {session_id} --limit plays=3 --notify 1 --killMessage "You have met your limit of 3 watches."
|
||||
```
|
||||
|
||||
### Limit user to total Plays/Watches in a specific library (Movies)
|
||||
_Completed play sessions_ \
|
||||
Triggers: Playback Start
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop watch --username {username} --sessionId {session_id} --limit plays=3 --section Movies --killMessage "You have met your limit of 3 watches."
|
||||
```
|
||||
|
||||
### Limit user to total time watching
|
||||
@ -29,15 +38,24 @@ Triggers: Playback Start
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop time --username {username} --sessionId {session_id} --limit days=3 --limit hours=10 --killMessage 'You have met your limit of 3 days and 10 hours.'
|
||||
--jbop time --username {username} --sessionId {session_id} --limit days=3 --limit hours=10 --killMessage "You have met your limit of 3 days and 10 hours."
|
||||
```
|
||||
|
||||
|
||||
### Limit user to total play sessions
|
||||
### Limit user to total play sessions for the day
|
||||
|
||||
Triggers: Playback Start
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop plays --username {username} --sessionId {session_id} --limit plays=3 --killMessage 'You have met your limit of 3 play sessions.'
|
||||
```
|
||||
--jbop plays --username {username} --sessionId {session_id} --days 0 --limit plays=3 --killMessage "You have met your limit of 3 play sessions."
|
||||
```
|
||||
|
||||
### Limit user to total time watching for the week, including duration of item starting
|
||||
|
||||
Triggers: Playback Start
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop time --username {username} --sessionId {session_id} --duration {duration} --days 7 --limit hours=10 --killMessage "You have met your weekly limit of 10 hours."
|
||||
```
|
||||
|
@ -6,7 +6,10 @@ Killing streams is a Plex Pass only feature. So these scripts will **only** work
|
||||
|
||||
### Kill transcodes
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
|
||||
Arguments:
|
||||
@ -59,6 +62,18 @@ Arguments:
|
||||
--jbop stream --username {username} --sessionId {session_id} --killMessage 'You are only allowed 3 streams.'
|
||||
```
|
||||
|
||||
### Limit User streams to one unique IP
|
||||
|
||||
Triggers: User Concurrent Streams
|
||||
Settings:
|
||||
* Notifications & Newsletters > Show Advanced > `User Concurrent Streams Notifications by IP Address` | `Checked`
|
||||
* Notifications & Newsletters > `User Concurrent Stream Threshold` | `2`
|
||||
|
||||
Arguments:
|
||||
```
|
||||
--jbop stream --username {username} --sessionId {session_id} --killMessage 'You are only allowed to stream from one location at a time.'
|
||||
```
|
||||
|
||||
### IP Whitelist
|
||||
|
||||
Triggers: Playback Start
|
||||
@ -81,7 +96,10 @@ Arguments:
|
||||
|
||||
### Kill transcode by library
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions:
|
||||
* \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
* \[ `Library Name` | `is` | `4K Movies` \]
|
||||
@ -93,7 +111,10 @@ Arguments:
|
||||
|
||||
### Kill transcode by original resolution
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions:
|
||||
* \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
* \[ `Video Resolution` | `is` | `1080 or 720`\]
|
||||
@ -105,7 +126,10 @@ Arguments:
|
||||
|
||||
### Kill transcode by bitrate
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions:
|
||||
* \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
* \[ `Bitrate` | `is greater than` | `4000` \]
|
||||
@ -137,7 +161,10 @@ Arguments:
|
||||
|
||||
### Kill transcodes and send a notification to agent 1
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
|
||||
Arguments:
|
||||
@ -147,7 +174,10 @@ Arguments:
|
||||
|
||||
### Kill transcodes using the default message
|
||||
|
||||
Triggers: Playback Start
|
||||
Triggers:
|
||||
* Playback Start
|
||||
* Transcode Decision Change
|
||||
|
||||
Conditions: \[ `Transcode Decision` | `is` | `transcode` \]
|
||||
|
||||
Arguments:
|
||||
@ -164,3 +194,58 @@ Arguments:
|
||||
```
|
||||
--jbop allStreams --userId {user_id} --notify 1 --killMessage 'Hey Bob, we need to talk!'
|
||||
```
|
||||
|
||||
### Rich Notifications (Discord or Slack)
|
||||
The following can be added to any of the above examples.
|
||||
|
||||
#### How it Works
|
||||
|
||||
Tautulli > Script Agent > Script > Tautulli > Webhook Agent > Discord/Slack
|
||||
|
||||
1. Tautulli's script agent is executed
|
||||
1. Script executes
|
||||
1. The script sends the JSON data to the webhook agent
|
||||
1. Webhook agent passes the information to Discord/Slack
|
||||
|
||||
#### Limitations
|
||||
* Due to [size](https://api.slack.com/docs/message-attachments#thumb_url) limitations by Slack. A thumbnail may not appear with every notification when using `--posterUrl {poster_url}`.
|
||||
* `allStreams` will not have poster images in the notifications.
|
||||
|
||||
#### Required arguments
|
||||
|
||||
* Discord: `--notify notifierID --richMessage discord`
|
||||
* Slack: `--notify notifierID --richMessage slack`
|
||||
|
||||
**_Note: The notifierID must be a Webhook in Tautulli_**
|
||||
|
||||
#### Optional arguments
|
||||
|
||||
```log
|
||||
--serverName {server_name} --plexUrl {plex_url} --posterUrl {poster_url} --richColor '#E5A00D'
|
||||
```
|
||||
|
||||
#### Webhook Setup
|
||||
1. Settings -> Notification Agents -> Add a new notification agent -> Webhook
|
||||
1. For the **Webhook URL** enter your Slack or Discord webhook URL. </br>
|
||||
Some examples:
|
||||
* Discord: [Intro to Webhooks](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks)
|
||||
* Slack: [Incoming Webhooks](https://api.slack.com/incoming-webhooks)
|
||||
1. **Webhook Method** - `POST`
|
||||
1. No triggers or any other configuration is needed. The script will pass the notifier the data to send to Discord/Slack.
|
||||
|
||||
<p>
|
||||
<img width="420" src="https://i.imgur.com/9iQZsq4.png">
|
||||
<img width="420" src="https://i.imgur.com/VvivqCX.png">
|
||||
</p>
|
||||
|
||||
### Debug
|
||||
|
||||
Add `--debug` to enable debug logging.
|
||||
|
||||
### Conditions considerations
|
||||
|
||||
#### Kill transcode variants
|
||||
|
||||
All examples use \[ `Transcode Decision` | `is` | `transcode` \] which will kill any variant of transcoding.
|
||||
If you want to allow audio or container transcoding and only drop video transcodes, your condition would change to
|
||||
\[ `Video Decision` | `is` | `transcode` \]
|
||||
|
@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Use Tautulli draw a map connecting Server to Clients based on IP addresses.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-l , --location Map location. choices: (NA, EU, World, Geo)
|
||||
-m , --map Map location. choices: (NA, EU, World, Geo)
|
||||
(default: NA)
|
||||
-c [], --count [] How many IPs to attempt to check.
|
||||
(default: 2)
|
||||
@ -23,19 +26,25 @@ optional arguments:
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import zip
|
||||
from builtins import str
|
||||
from builtins import range
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
import argparse
|
||||
import numpy as np
|
||||
# import numpy as np
|
||||
import time
|
||||
import webbrowser
|
||||
import re
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = '' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
@ -209,7 +218,7 @@ def get_geo_dict(length, users):
|
||||
|
||||
def get_geojson_dict(user_locations):
|
||||
locs = []
|
||||
for username, locations in user_locations.iteritems():
|
||||
for username, locations in user_locations.items():
|
||||
for location in locations:
|
||||
try:
|
||||
locs.append({
|
||||
@ -259,7 +268,7 @@ def draw_map(map_type, geo_dict, filename, headless, leg_choice):
|
||||
import matplotlib.pyplot as plt
|
||||
from mpl_toolkits.basemap import Basemap
|
||||
|
||||
## Map stuff ##
|
||||
# ## Map stuff ##
|
||||
plt.figure(figsize=(16, 9), dpi=100, frameon=False)
|
||||
lon_r = 0
|
||||
lon_l = 0
|
||||
@ -296,7 +305,7 @@ def draw_map(map_type, geo_dict, filename, headless, leg_choice):
|
||||
if key == SERVER_FRIENDLY:
|
||||
color = '#FFAC05'
|
||||
marker = '*'
|
||||
markersize = 10
|
||||
markersize = 8
|
||||
zord = 3
|
||||
alph = 1
|
||||
else:
|
||||
@ -307,9 +316,9 @@ def draw_map(map_type, geo_dict, filename, headless, leg_choice):
|
||||
print('Platform: {} is missing from PLATFORM_COLORS. Using DEFAULT_COLOR.'.format(data['platform']))
|
||||
marker = '.'
|
||||
if data['play_count'] >= 100:
|
||||
markersize = (data['play_count'] * .1)
|
||||
markersize = (data['play_count'] * .01)
|
||||
elif data['play_count'] <= 2:
|
||||
markersize = 2
|
||||
markersize = 1
|
||||
else:
|
||||
markersize = 2
|
||||
zord = 2
|
||||
@ -345,9 +354,9 @@ def draw_map(map_type, geo_dict, filename, headless, leg_choice):
|
||||
0))
|
||||
labels = labels[idx:] + labels[:idx]
|
||||
handles = handles[idx:] + handles[:idx]
|
||||
by_label = OrderedDict(zip(labels, handles))
|
||||
by_label = OrderedDict(list(zip(labels, handles)))
|
||||
|
||||
leg = plt.legend(by_label.values(), by_label.keys(), fancybox=True, fontsize='x-small',
|
||||
leg = plt.legend(list(by_label.values()), list(by_label.keys()), fancybox=True, fontsize='x-small',
|
||||
numpoints=1, title="Legend", labelspacing=1., borderpad=1.5, handletextpad=2.)
|
||||
if leg:
|
||||
lleng = len(leg.legendHandles)
|
||||
@ -355,7 +364,7 @@ def draw_map(map_type, geo_dict, filename, headless, leg_choice):
|
||||
leg.legendHandles[i]._legmarker.set_markersize(10)
|
||||
leg.legendHandles[i]._legmarker.set_alpha(1)
|
||||
leg.get_title().set_color('#7B777C')
|
||||
leg.draggable()
|
||||
leg.set_draggable(state=True)
|
||||
leg.get_frame().set_facecolor('#2C2C2C')
|
||||
for text in leg.get_texts():
|
||||
plt.setp(text, color='#A5A5A7')
|
||||
@ -428,22 +437,9 @@ if __name__ == '__main__':
|
||||
if opts.map == 'Geo':
|
||||
geojson = get_geojson_dict(geo_json)
|
||||
print("\n")
|
||||
geojson_file = '{}.geojson'.format(''.join(opts.filename))
|
||||
with open(geojson_file, 'w') as fp:
|
||||
json.dump(geojson, fp, indent=4, sort_keys=True)
|
||||
|
||||
r = requests.post("https://api.github.com/gists",
|
||||
json={
|
||||
"description": title_string,
|
||||
"files": {
|
||||
'{}.geojson'.format(filename): {
|
||||
"content": json.dumps(geojson, indent=4)
|
||||
}
|
||||
}
|
||||
},
|
||||
headers={
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
|
||||
print(r.json()['html_url'])
|
||||
if not opts.headless:
|
||||
webbrowser.open(r.json()['html_url'])
|
||||
else:
|
||||
draw_map(opts.map, geo_json, filename, opts.headless, opts.legend)
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
# Maps
|
||||
|
||||
Maps are created with either Matplotlib/Basemap or as a geojson file on an anonymous gist.
|
||||
@ -46,16 +45,14 @@ Choose which map type you'd like by using the `-l` argument:
|
||||
|
||||
- [x] Add check for user count in user_table to allow for greater than 25 users - [Pull](https://github.com/blacktwin/JBOPS/pull/3)
|
||||
- [x] If platform is missing from PLATFORM_COLORS use DEFAULT_COLOR - [Pull](https://github.com/blacktwin/JBOPS/pull/4)
|
||||
- [x] Add arg to allow for runs in headless (mpl.use("Agg"))
|
||||
- [x] Add arg to allow for runs in headless (mpl.use("Agg"))
|
||||
- [x] Add pass on N/A values for Lon/Lat - [Pull](https://github.com/blacktwin/JBOPS/pull/2)
|
||||
|
||||
### Feature updates:
|
||||
|
||||
- [ ] Add arg for legend (best, none, axes)
|
||||
- [ ] Add arg for legend (best, none, axes, top_left, top_center, etc.)
|
||||
- [ ] UI, toggles, interactive mode
|
||||
- [ ] Add arg to group legend items by city and/or region
|
||||
- [ ] Find server's external IP, geolocation. Allow custom location to override
|
||||
- [ ] Add arg for tracert visualization from server to client
|
||||
- [ ] Animate tracert visualization? gif?
|
||||
- [ ] UI, toggles, interactive mode
|
||||
|
||||
|
||||
|
||||
|
8
maps/requirements.txt
Normal file
8
maps/requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
#---------------------------------------------------------
|
||||
# Potential requirements.
|
||||
# pip install -r requirements.txt
|
||||
#---------------------------------------------------------
|
||||
requests
|
||||
matplotlib
|
||||
numpy
|
||||
basemap
|
@ -1,10 +1,16 @@
|
||||
"""
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Find what was added TFRAME ago and not watched and notify admin using Tautulli.
|
||||
|
||||
TAUTULLI_URL + delete_media_info_cache?section_id={section_id}
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import object
|
||||
from builtins import str
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
@ -12,7 +18,7 @@ import time
|
||||
TFRAME = 1.577e+7 # ~ 6 months in seconds
|
||||
TODAY = time.time()
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = '' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8183/' # Your Tautulli URL
|
||||
LIBRARY_NAMES = ['Movies', 'TV Shows'] # Name of libraries you want to check.
|
||||
@ -162,10 +168,10 @@ for library in libraries:
|
||||
# Find movie rating_key.
|
||||
show_lst += [int(lib.rating_key)]
|
||||
except Exception as e:
|
||||
print "Rating_key failed: {e}".format(e=e)
|
||||
print("Rating_key failed: {e}".format(e=e))
|
||||
|
||||
except Exception as e:
|
||||
print "Library media info failed: {e}".format(e=e)
|
||||
print("Library media info failed: {e}".format(e=e))
|
||||
|
||||
for show in show_lst:
|
||||
try:
|
||||
@ -181,7 +187,7 @@ for show in show_lst:
|
||||
u" not been watched.<d/t> <dd>File location: {x.file}</dd> <br>".format(x=meta, when=added)]
|
||||
|
||||
except Exception as e:
|
||||
print "Metadata failed. Likely end of range: {e}".format(e=e)
|
||||
print("Metadata failed. Likely end of range: {e}".format(e=e))
|
||||
|
||||
if notify_lst:
|
||||
BODY_TEXT = """\
|
||||
|
@ -1,359 +0,0 @@
|
||||
"""
|
||||
Send an email with what was added to Plex in the past week using Tautulli.
|
||||
Email includes title (TV: Show Name: Episode Name; Movie: Movie Title), time added, image, and summary.
|
||||
|
||||
Uses:
|
||||
notify_added_lastweek.py -t poster -d 1 -u all -i user1 user2 -s 250 100
|
||||
# email all users expect user1 & user2 what was added in the last day using posters that are 250x100
|
||||
|
||||
notify_added_lastweek.py -t poster -d 7 -u all
|
||||
# email all users what was added in the last 7 days(week) using posters that are default sized
|
||||
|
||||
notify_added_lastweek.py -t poster -d 7 -u all -s 1000 500
|
||||
# email all users what was added in the last 7 days(week) using posters that are 1000x500
|
||||
|
||||
notify_added_lastweek.py -t art -d 7 -u user1
|
||||
# email user1 & self what was added in the last 7 days(week) using artwork that is default sized
|
||||
|
||||
notify_added_lastweek.py -t art -d 7
|
||||
# email self what was added in the last 7 days(week) using artwork that is default sized
|
||||
|
||||
"""
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.image import MIMEImage
|
||||
import email.utils
|
||||
import smtplib
|
||||
import urllib
|
||||
import cgi
|
||||
import uuid
|
||||
import argparse
|
||||
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = '' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
LIBRARY_NAMES = ['Movies', 'TV Shows'] # Name of libraries you want to check.
|
||||
|
||||
# Email settings
|
||||
name = '' # Your name
|
||||
sender = '' # From email address
|
||||
to = [sender] # Whoever you want to email [sender, 'name@example.com']
|
||||
# Emails will be sent as BCC.
|
||||
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
|
||||
email_port = 587 # Email port (Gmail: 587)
|
||||
email_username = '' # Your email username
|
||||
email_password = '' # Your email password
|
||||
email_subject = 'Tautulli Added Last {} day(s) Notification' #The email subject
|
||||
|
||||
# Default sizing for pictures
|
||||
# Poster
|
||||
poster_h = 205
|
||||
poster_w = 100
|
||||
# Artwork
|
||||
art_h = 100
|
||||
art_w = 205
|
||||
|
||||
## /EDIT THESE SETTINGS ##
|
||||
|
||||
class METAINFO(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.added_at = d['added_at']
|
||||
self.parent_rating_key = d['parent_rating_key']
|
||||
self.title = d['title']
|
||||
self.rating_key = d['rating_key']
|
||||
self.media_type = d['media_type']
|
||||
self.grandparent_title = d['grandparent_title']
|
||||
self.thumb = d['art']
|
||||
self.summary = d['summary']
|
||||
|
||||
|
||||
def get_recent(section_id, start, count):
|
||||
# Get the metadata for a media item. Count matters!
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'start': str(start),
|
||||
'count': str(count),
|
||||
'section_id': section_id,
|
||||
'cmd': 'get_recently_added'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
|
||||
if response['response']['result'] == 'success':
|
||||
res_data = response['response']['data']['recently_added']
|
||||
|
||||
return res_data
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_recently_added' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_metadata(rating_key):
|
||||
# Get the metadata for a media item.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'rating_key': rating_key,
|
||||
'cmd': 'get_metadata',
|
||||
'media_info': True}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
if response['response']['result'] == 'success':
|
||||
res_data = response['response']['data']
|
||||
|
||||
return METAINFO(data=res_data)
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_libraries_table():
|
||||
# Get the data on the Tautulli libraries table.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_libraries_table'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
res_data = response['response']['data']['data']
|
||||
return [d['section_id'] for d in res_data if d['section_name'] in LIBRARY_NAMES]
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_libraries_table' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def update_library_media_info(section_id):
|
||||
# Get the data on the Tautulli media info tables.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_library_media_info',
|
||||
'section_id': section_id,
|
||||
'refresh': True}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.status_code
|
||||
if response != 200:
|
||||
print(r.content)
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'update_library_media_info' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_pms_image_proxy(thumb):
|
||||
# Gets an image from the PMS and saves it to the image cache directory.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'pms_image_proxy',
|
||||
'img': thumb}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload, stream=True)
|
||||
return r.url
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_users_tables' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_users():
|
||||
# Get the user list from Tautulli.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_users'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
res_data = response['response']['data']
|
||||
return [d for d in res_data]
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_rating_keys(TODAY, LASTDATE):
|
||||
|
||||
recent_lst = []
|
||||
# Get the rating_key for what was recently added
|
||||
count = 25
|
||||
for section_id in glt:
|
||||
start = 0
|
||||
|
||||
while True:
|
||||
# Assume all items will be returned in descending order of added_at
|
||||
recent_items = get_recent(section_id, start, count)
|
||||
|
||||
if all([recent_items]):
|
||||
start += count
|
||||
for item in recent_items:
|
||||
if LASTDATE <= int(item['added_at']) <= TODAY:
|
||||
recent_lst.append(item['rating_key'])
|
||||
continue
|
||||
elif not all([recent_items]):
|
||||
break
|
||||
|
||||
start += count
|
||||
if recent_lst:
|
||||
return recent_lst
|
||||
sys.stderr.write("Recently Added list: {0}.".format(recent_lst))
|
||||
exit()
|
||||
|
||||
|
||||
def build_html(rating_key, height, width, pic_type):
|
||||
|
||||
meta = get_metadata(str(rating_key))
|
||||
|
||||
added = time.ctime(float(meta.added_at))
|
||||
# Pull image url
|
||||
thumb_url = "{}.jpeg".format(get_pms_image_proxy(meta.thumb))
|
||||
if pic_type == 'poster':
|
||||
thumb_url = thumb_url.replace('%2Fart%', '%2Fposter%')
|
||||
image_name = "{}.jpg".format(str(rating_key))
|
||||
# Saving image in current path
|
||||
urllib.urlretrieve(thumb_url, image_name)
|
||||
image = dict(title=meta.rating_key, path=image_name, cid=str(uuid.uuid4()))
|
||||
if meta.grandparent_title == '' or meta.media_type == 'movie':
|
||||
# Movies
|
||||
notify = u"<dt>{x.title} ({x.rating_key}) was added {when}.</dt>" \
|
||||
u"</dt> <dd> <table> <tr> <th>" \
|
||||
'<img src="cid:{cid}" alt="{alt}" width="{width}" height="{height}"> </th>' \
|
||||
u" <th id=t11> {x.summary} </th> </tr> </table> </dd> <br>" \
|
||||
.format(x=meta, when=added, alt=cgi.escape(meta.rating_key), quote=True, width=width, height=height,**image)
|
||||
else:
|
||||
# Shows
|
||||
notify = u"<dt>{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when}." \
|
||||
u"</dt> <dd> <table> <tr> <th>" \
|
||||
'<img src="cid:{cid}" alt="{alt}" width="{width}" height="{height}"> </th>' \
|
||||
u" <th id=t11> {x.summary} </th> </tr> </table> </dd> <br>" \
|
||||
.format(x=meta, when=added, alt=cgi.escape(meta.rating_key), quote=True, width=width, height=height, **image)
|
||||
|
||||
image_text = MIMEText(u'[image: {title}]'.format(**image), 'plain', 'utf-8')
|
||||
|
||||
return image_text, image, notify
|
||||
|
||||
|
||||
def send_email(msg_text_lst, notify_lst, image_lst, to, days):
|
||||
"""
|
||||
Using info found here: http://stackoverflow.com/a/20485764/7286812
|
||||
to accomplish emailing inline images
|
||||
"""
|
||||
msg_html = MIMEText("""\
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
th#t11 {{ padding: 6px; vertical-align: top; text-align: left; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hi!<br>
|
||||
<br>Below is the list of content added to Plex's {LIBRARY_NAMES} this week.<br>
|
||||
<dl>
|
||||
{notify_lst}
|
||||
</dl>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""".format(notify_lst="\n".join(notify_lst).encode("utf-8"), LIBRARY_NAMES=" & ".join(LIBRARY_NAMES)
|
||||
, quote=True, ), 'html', 'utf-8')
|
||||
|
||||
message = MIMEMultipart('related')
|
||||
message['Subject'] = email_subject.format(days)
|
||||
message['From'] = email.utils.formataddr((name, sender))
|
||||
message_alternative = MIMEMultipart('alternative')
|
||||
message.attach(message_alternative)
|
||||
|
||||
for msg_text in msg_text_lst:
|
||||
message_alternative.attach(msg_text)
|
||||
|
||||
message_alternative.attach(msg_html)
|
||||
|
||||
for img in image_lst:
|
||||
with open(img['path'], 'rb') as file:
|
||||
message_image_lst = [MIMEImage(file.read(), name=os.path.basename(img['path']))]
|
||||
|
||||
for msg in message_image_lst:
|
||||
message.attach(msg)
|
||||
msg.add_header('Content-ID', '<{}>'.format(img['cid']))
|
||||
|
||||
mailserver = smtplib.SMTP(email_server, email_port)
|
||||
mailserver.ehlo()
|
||||
mailserver.starttls()
|
||||
mailserver.ehlo()
|
||||
mailserver.login(email_username, email_password)
|
||||
mailserver.sendmail(sender, to, message.as_string())
|
||||
mailserver.quit()
|
||||
print('Email sent')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Send an email with what was added to Plex in the past week using Tautulli.")
|
||||
parser.add_argument('-t', '--type', help='Metadata picture type from Plex.',
|
||||
required= True, choices=['art', 'poster'])
|
||||
parser.add_argument('-s', '--size', help='Metadata picture size from Plex {Height Width}.', nargs='*')
|
||||
parser.add_argument('-d', '--days', help='Time frame for which to check recently added to Plex.',
|
||||
required= True, type=int)
|
||||
parser.add_argument('-u', '--users', help='Which users from Plex will be emailed.',
|
||||
nargs='+', default='self', type=str)
|
||||
parser.add_argument('-i', '--ignore', help='Which users from Plex to ignore.',
|
||||
nargs='+', default='None', type=str)
|
||||
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
TODAY = int(time.time())
|
||||
LASTDATE = int(TODAY - opts.days * 24 * 60 * 60)
|
||||
|
||||
# Image sizing based on type or custom size
|
||||
if opts.type == 'poster' and not opts.size:
|
||||
height = poster_h
|
||||
width = poster_w
|
||||
elif opts.size:
|
||||
height = opts.size[0]
|
||||
width = opts.size[1]
|
||||
else:
|
||||
height = art_h
|
||||
width = art_w
|
||||
|
||||
# Find the libraries from LIBRARY_NAMES
|
||||
glt = [lib for lib in get_libraries_table()]
|
||||
|
||||
# Update media info for libraries.
|
||||
[update_library_media_info(i) for i in glt]
|
||||
|
||||
# Gather all users email addresses
|
||||
if opts.users == ['all']:
|
||||
[to.append(x['email']) for x in get_users() if x['email'] != '' and x['email'] not in to
|
||||
and x['username'] not in opts.ignore]
|
||||
elif opts.users != ['all'] and opts.users != 'self':
|
||||
for get_users in get_users():
|
||||
for arg_users in opts.users:
|
||||
if arg_users in get_users['username']:
|
||||
to = to + [str(get_users['email'])]
|
||||
print('Sending email(s) to {}'.format(', '.join(to)))
|
||||
|
||||
# Gather rating_keys on recently added media.
|
||||
rating_keys_lst = get_rating_keys(TODAY, LASTDATE)
|
||||
|
||||
# Build html elements from rating_key
|
||||
image_lst = []
|
||||
msg_text_lst = []
|
||||
notify_lst = []
|
||||
|
||||
build_parts = [build_html(rating_key, height, width, opts.type) for rating_key in sorted(rating_keys_lst)]
|
||||
for parts in build_parts:
|
||||
msg_text_lst.append(parts[0])
|
||||
image_lst.append(parts[1])
|
||||
notify_lst.append(parts[2])
|
||||
|
||||
# Send email
|
||||
send_email(msg_text_lst, notify_lst, image_lst, to, opts.days)
|
||||
|
||||
# Delete images in current path
|
||||
for img in image_lst:
|
||||
os.remove(img['path'])
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Delay Notification Agent message for concurrent streams
|
||||
|
||||
@ -13,13 +16,17 @@ Tautulli > Settings > Notification Agents > Scripts > Gear icon:
|
||||
|
||||
Tautulli Settings > Notification Agents > Scripts (Gear) > Script Timeout: 0 to disable or set to > 180
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from past.utils import old_div
|
||||
import requests
|
||||
import sys
|
||||
import argparse
|
||||
from time import sleep
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = '' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
CONCURRENT_TOTAL = 2
|
||||
@ -44,7 +51,7 @@ BODY_TEXT = """\
|
||||
|
||||
|
||||
def get_activity():
|
||||
# Get the current activity on the PMS.
|
||||
"""Get the current activity on the PMS."""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_activity'}
|
||||
|
||||
@ -60,10 +67,10 @@ def get_activity():
|
||||
|
||||
|
||||
def send_notification(subject_text, body_text):
|
||||
# Format notification text
|
||||
"""Format notification text."""
|
||||
try:
|
||||
subject = subject_text.format(p=p, total=cc_total)
|
||||
body = body_text.format(p=p, total=cc_total, time=TIMEOUT / 60)
|
||||
body = body_text.format(p=p, total=cc_total, time=old_div(TIMEOUT, 60))
|
||||
|
||||
except LookupError as e:
|
||||
sys.stderr.write("Unable to substitute '{0}' in the notification subject or body".format(e))
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Notify users of recently added episode to show that they have watched at least LIMIT times via email.
|
||||
Also notify users of new movies.
|
||||
@ -14,7 +17,10 @@ Tautulli > Settings > Notification Agents > Scripts > Bell icon:
|
||||
Tautulli > Settings > Notification Agents > Scripts > Gear icon:
|
||||
Recently Added: notify_fav_tv_all_movie.py
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import object
|
||||
import requests
|
||||
from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
@ -22,11 +28,11 @@ import smtplib
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
IGNORE_LST = ['123456', '123456'] # User_ids
|
||||
IGNORE_LST = ['123456', '123456'] # User_ids
|
||||
LIMIT = 3
|
||||
|
||||
# Email settings
|
||||
@ -74,6 +80,7 @@ TV_BODY = """\
|
||||
|
||||
user_dict = {}
|
||||
|
||||
|
||||
class Users(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
@ -108,6 +115,7 @@ def get_user(user_id):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_users():
|
||||
# Get the user list from Tautulli.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -134,8 +142,9 @@ def get_history(showkey):
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
res_data = response['response']['data']['data']
|
||||
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1
|
||||
and d['media_type'].lower() in ('episode', 'show')]
|
||||
return [UserHIS(data=d) for d in res_data
|
||||
if d['watched_status'] == 1 and
|
||||
d['media_type'].lower() in ('episode', 'show')]
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
@ -177,7 +186,7 @@ def get_email(show):
|
||||
|
||||
|
||||
def send_email(to, email_subject, body_html):
|
||||
### Do not edit below ###
|
||||
# ## Do not edit below ###
|
||||
message = MIMEText(body_html, 'html')
|
||||
message['Subject'] = email_subject
|
||||
message['From'] = email.utils.formataddr((name, sender))
|
||||
@ -188,7 +197,7 @@ def send_email(to, email_subject, body_html):
|
||||
mailserver.login(email_username, email_password)
|
||||
mailserver.sendmail(sender, to, message.as_string())
|
||||
mailserver.quit()
|
||||
print 'Email sent'
|
||||
print('Email sent')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@ -237,11 +246,11 @@ if __name__ == '__main__':
|
||||
|
||||
if p.media_type == 'movie':
|
||||
email_subject = MOVIE_SUBJECT.format(p=p)
|
||||
to = filter(None, [x['email'] for x in get_users() if x['user_id'] not in IGNORE_LST])
|
||||
to = [_f for _f in [x['email'] for x in get_users() if x['user_id'] not in IGNORE_LST] if _f]
|
||||
body_html = MOVIE_BODY.format(p=p)
|
||||
send_email(to, email_subject, body_html)
|
||||
|
||||
elif p.media_type in ['show', 'season', 'episode']:
|
||||
elif p.media_type in ['show', 'season', 'episode']:
|
||||
email_subject = TV_SUBJECT.format(p=p)
|
||||
to = get_email(int(p.grandparent_rating_key))
|
||||
body_html = TV_BODY.format(p=p)
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Pulling together User IP information and Email.
|
||||
|
||||
@ -14,11 +17,13 @@ Arguments passed from Tautulli
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from builtins import object
|
||||
import argparse
|
||||
import requests
|
||||
import sys
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = '' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
NOTIFIER_ID = 12 # The notification notifier ID
|
||||
@ -38,9 +43,9 @@ BODY_TEXT = """\
|
||||
<head></head>
|
||||
<body>
|
||||
<p>Hi!<br>
|
||||
<br><a href="mailto:{u.email}"><img src="{u.user_thumb}" alt="Poster unavailable" height="50" width="50"></a>
|
||||
<br><a href="mailto:{u.email}"><img src="{u.user_thumb}" alt="Poster unavailable" height="50" width="50"></a>
|
||||
{p.user} has watched {p.media_type}:{p.title} from a new IP address: {p.ip_address}<br>
|
||||
<br>On {p.platform}[{p.player}] in
|
||||
<br>On {p.platform}[{p.player}] in
|
||||
<a href="http://maps.google.com/?q={g.city},{g.country},{g.postal_code}">{g.city}, {g.country} {g.postal_code}</a>
|
||||
at {p.timestamp} on {p.datestamp}<br>
|
||||
<br><br>
|
||||
@ -68,7 +73,7 @@ class UserEmail(object):
|
||||
|
||||
|
||||
def get_user_ip_addresses(user_id='', ip_address=''):
|
||||
# Get the user IP list from Tautulli
|
||||
"""Get the user IP list from Tautulli."""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_user_ips',
|
||||
'user_id': user_id,
|
||||
@ -98,7 +103,7 @@ def get_user_ip_addresses(user_id='', ip_address=''):
|
||||
|
||||
|
||||
def get_geoip_info(ip_address=''):
|
||||
# Get the geo IP lookup from Tautulli
|
||||
"""Get the geo IP lookup from Tautulli."""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_geoip_lookup',
|
||||
'ip_address': ip_address}
|
||||
@ -122,7 +127,7 @@ def get_geoip_info(ip_address=''):
|
||||
|
||||
|
||||
def get_user_email(user_id=''):
|
||||
# Get the user email from Tautulli
|
||||
"""Get the user email from Tautulli."""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_user',
|
||||
'user_id': user_id}
|
||||
@ -146,7 +151,7 @@ def get_user_email(user_id=''):
|
||||
|
||||
|
||||
def send_notification(arguments=None, geodata=None, useremail=None):
|
||||
# Format notification text
|
||||
"""Format notification text."""
|
||||
try:
|
||||
subject = SUBJECT_TEXT.format(p=arguments, g=geodata, u=useremail)
|
||||
body = BODY_TEXT.format(p=arguments, g=geodata, u=useremail)
|
||||
|
@ -1,102 +0,0 @@
|
||||
"""
|
||||
Tautulli > Settings > Notification Agents > Scripts > Bell icon:
|
||||
[X] Notify on Recently Added
|
||||
Tautulli > Settings > Notification Agents > Scripts > Gear icon:
|
||||
Recently Added: notify_on_added.py
|
||||
|
||||
Tautulli > Settings > Notifications > Script > Script Arguments:
|
||||
-sn {show_name} -ena {episode_name} -ssn {season_num00} -enu {episode_num00} -srv {server_name} -med {media_type} -pos {poster_url} -tt {title} -sum {summary} -lbn {library_name}
|
||||
|
||||
You can add more arguments if you want more details in the email body
|
||||
"""
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
import smtplib
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-sn', '--show_name', action='store', default='',
|
||||
help='The name of the TV show')
|
||||
parser.add_argument('-ena', '--episode_name', action='store', default='',
|
||||
help='The name of the episode')
|
||||
parser.add_argument('-ssn', '--season_num', action='store', default='',
|
||||
help='The season number of the TV show')
|
||||
parser.add_argument('-enu', '--episode_num', action='store', default='',
|
||||
help='The episode number of the TV show')
|
||||
parser.add_argument('-srv', '--plex_server', action='store', default='',
|
||||
help='The name of the Plex server')
|
||||
parser.add_argument('-med', '--show_type', action='store', default='',
|
||||
help='The type of media')
|
||||
parser.add_argument('-pos', '--poster', action='store', default='',
|
||||
help='The poster url')
|
||||
parser.add_argument('-tt', '--title', action='store', default='',
|
||||
help='The title of the TV show')
|
||||
parser.add_argument('-sum', '--summary', action='store', default='',
|
||||
help='The summary of the TV show')
|
||||
parser.add_argument('-lbn', '--library_name', action='store', default='',
|
||||
help='The name of the TV show')
|
||||
p = parser.parse_args()
|
||||
|
||||
# Edit user@email.com and shows
|
||||
users = [{'email': 'user1@gmail.com',
|
||||
'shows': ('show1', 'show2')
|
||||
},
|
||||
{'email': 'user2@gmail.com',
|
||||
'shows': ('show1', 'show2', 'show3')
|
||||
},
|
||||
{'email': 'user3@gmail.com',
|
||||
'shows': ('show1', 'show2', 'show3', 'show4')
|
||||
}]
|
||||
|
||||
# Kill script now if show_name is not in lists
|
||||
too = list('Match' for u in users if p.show_name in u['shows'])
|
||||
if not too:
|
||||
print 'Kill script now show_name is not in lists'
|
||||
exit()
|
||||
|
||||
# Join email addresses
|
||||
to = list([u['email'] for u in users if p.show_name in u['shows']])
|
||||
|
||||
# Email settings
|
||||
name = 'Tautulli' # Your name
|
||||
sender = 'sender' # From email address
|
||||
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
|
||||
email_port = 587 # Email port (Gmail: 587)
|
||||
email_username = 'email' # Your email username
|
||||
email_password = 'password' # Your email password
|
||||
email_subject = 'New episode for ' + p.show_name + ' is available on ' + p.plex_server # The email subject
|
||||
|
||||
# Detailed body for tv shows
|
||||
show_html = """\
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>Hi!<br>
|
||||
{p.show_name} S{p.season_num} - E{p.episode_num} -- {p.episode_name} -- was recently added to {p.library_name} on PLEX
|
||||
<br><br>
|
||||
<br> {p.summary} <br>
|
||||
<br><img src="{p.poster}" alt="Poster unavailable" height="150" width="102"><br>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""".format(p=p)
|
||||
|
||||
### Do not edit below ###
|
||||
# Check to see whether it is a tv show
|
||||
if p.show_type.lower() == 'show' or p.show_type.lower() == 'episode':
|
||||
message = MIMEText(show_html, 'html')
|
||||
message['Subject'] = email_subject
|
||||
message['From'] = email.utils.formataddr((name, sender))
|
||||
|
||||
|
||||
mailserver = smtplib.SMTP(email_server, email_port)
|
||||
mailserver.starttls()
|
||||
mailserver.ehlo()
|
||||
mailserver.login(email_username, email_password)
|
||||
mailserver.sendmail(sender, to, message.as_string())
|
||||
mailserver.quit()
|
||||
else:
|
||||
exit()
|
93
notify/notify_recently_aired.py
Normal file
93
notify/notify_recently_aired.py
Normal file
@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Notify only if recently aired/released
|
||||
Author: Blacktwin
|
||||
Requires: requests
|
||||
|
||||
Enabling Scripts in Tautulli:
|
||||
Tautulli > Settings > Notification Agents > Add a Notification Agent > Script
|
||||
|
||||
Configuration:
|
||||
Tautulli > Settings > Notification Agents > New Script > Configuration:
|
||||
|
||||
Script Name: notify_recently_aired.py
|
||||
Set Script Timeout: Default
|
||||
Description: Notify only if recently aired/released
|
||||
Save
|
||||
|
||||
Triggers:
|
||||
Tautulli > Settings > Notification Agents > New Script > Triggers:
|
||||
|
||||
Check: Recently Added
|
||||
Save
|
||||
|
||||
Conditions:
|
||||
Tautulli > Settings > Notification Agents > New Script > Conditions:
|
||||
|
||||
Set Conditions: [{condition} | {operator} | {value} ]
|
||||
Save
|
||||
|
||||
Script Arguments:
|
||||
Tautulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
Select: Recently Added
|
||||
Arguments: {air_date} or {release_date} {rating_key}
|
||||
|
||||
Save
|
||||
Close
|
||||
|
||||
Note:
|
||||
You'll need another notification agent to use for actually sending the notification.
|
||||
The notifier_id in the edit section will need to be this other notification agent you intend to use.
|
||||
It does not have to be an active notification agent, just setup.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL)
|
||||
TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
|
||||
|
||||
# Edit
|
||||
date_format = "%Y-%m-%d"
|
||||
RECENT_DAYS = 3
|
||||
NOTIFIER_ID = 34
|
||||
# /Edit
|
||||
|
||||
air_date = sys.argv[1]
|
||||
rating_key = int(sys.argv[2])
|
||||
|
||||
aired_date = datetime.strptime(air_date, date_format)
|
||||
today = date.today()
|
||||
delta = today - aired_date.date()
|
||||
|
||||
|
||||
def notify_recently_added(rating_key, notifier_id):
|
||||
# Get the metadata for a media item.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'rating_key': rating_key,
|
||||
'notifier_id': notifier_id,
|
||||
'cmd': 'notify_recently_added'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
sys.stdout.write(response["response"]["message"])
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'notify_recently_added' request failed: {0}.".format(e))
|
||||
pass
|
||||
|
||||
|
||||
if delta.days < RECENT_DAYS:
|
||||
notify_recently_added(rating_key, NOTIFIER_ID)
|
||||
else:
|
||||
print("Not recent enough, no notification to be sent.")
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Notify users of recently added episode to show that they have watched at least LIMIT times via email.
|
||||
Block users with IGNORE_LST.
|
||||
@ -13,7 +16,10 @@ Tautulli > Settings > Notification Agents > Scripts > Bell icon:
|
||||
Tautulli > Settings > Notification Agents > Scripts > Gear icon:
|
||||
Recently Added: notify_user_favorite.py
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import object
|
||||
import requests
|
||||
from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
@ -21,11 +27,11 @@ import smtplib
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
IGNORE_LST = [123456, 123456] # User_ids
|
||||
IGNORE_LST = [123456, 123456] # User_ids
|
||||
LIMIT = 3
|
||||
|
||||
# Email settings
|
||||
@ -38,6 +44,7 @@ email_password = '' # Your email password
|
||||
|
||||
user_dict = {}
|
||||
|
||||
|
||||
class Users(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
@ -74,7 +81,10 @@ def get_user(user_id):
|
||||
|
||||
|
||||
def get_history(showkey):
|
||||
# Get the user history from Tautulli. Length matters!
|
||||
"""Get the user history from Tautulli.
|
||||
|
||||
Length matters!
|
||||
"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_history',
|
||||
'grandparent_rating_key': showkey,
|
||||
@ -84,8 +94,9 @@ def get_history(showkey):
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
res_data = response['response']['data']['data']
|
||||
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1
|
||||
and d['media_type'].lower() in ('episode', 'show')]
|
||||
return [UserHIS(data=d) for d in res_data
|
||||
if d['watched_status'] == 1 and
|
||||
d['media_type'].lower() in ('episode', 'show')]
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
@ -188,7 +199,7 @@ if __name__ == '__main__':
|
||||
</html>
|
||||
""".format(p=p)
|
||||
|
||||
### Do not edit below ###
|
||||
# ## Do not edit below ###
|
||||
message = MIMEText(show_html, 'html')
|
||||
message['Subject'] = email_subject
|
||||
message['From'] = email.utils.formataddr((name, sender))
|
||||
@ -199,4 +210,4 @@ if __name__ == '__main__':
|
||||
mailserver.login(email_username, email_password)
|
||||
mailserver.sendmail(sender, to, message.as_string())
|
||||
mailserver.quit()
|
||||
print 'Email sent'
|
||||
print('Email sent')
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Pulling together User IP information and Email.
|
||||
Enable the API under Settings > Access Control and remember your API key.
|
||||
@ -6,6 +9,9 @@ Set api_sql = 1 in the config file.
|
||||
Restart Tautulli.
|
||||
Place in Playback Start
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from builtins import object
|
||||
import argparse
|
||||
import requests
|
||||
import sys
|
||||
@ -13,9 +19,9 @@ from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
import smtplib
|
||||
|
||||
## -sn {show_name} -ena {episode_name} -ssn {season_num00} -enu {episode_num00} -srv {server_name} -med {media_type} -pos {poster_url} -tt {title} -sum {summary} -lbn {library_name} -ip {ip_address} -us {user} -uid {user_id} -pf {platform} -pl {player} -da {datestamp} -ti {timestamp}
|
||||
# ## -sn {show_name} -ena {episode_name} -ssn {season_num00} -enu {episode_num00} -srv {server_name} -med {media_type} -pos {poster_url} -tt {title} -sum {summary} -lbn {library_name} -ip {ip_address} -us {user} -uid {user_id} -pf {platform} -pl {player} -da {datestamp} -ti {timestamp}
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'xxxxxxxxxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
@ -44,17 +50,20 @@ BODY_TEXT = """\
|
||||
"""
|
||||
|
||||
# Email settings
|
||||
name = '' # Your name
|
||||
sender = '' # From email address
|
||||
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
|
||||
name = '' # Your name
|
||||
sender = '' # From email address
|
||||
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
|
||||
email_port = 587 # Email port (Gmail: 587)
|
||||
email_username = '' # Your email username
|
||||
email_password = '' # Your email password
|
||||
email_username = '' # Your email username
|
||||
email_password = '' # Your email password
|
||||
email_subject = "New IP has been detected using Plex."
|
||||
|
||||
IGNORE_LST = ['123456', '123456'] # User_id
|
||||
IGNORE_LST = ['123456', '123456'] # User_id
|
||||
|
||||
|
||||
# ##Geo Space##
|
||||
|
||||
|
||||
##Geo Space##
|
||||
class GeoData(object):
|
||||
def __init__(self, data=None):
|
||||
data = data or {}
|
||||
@ -62,22 +71,28 @@ class GeoData(object):
|
||||
self.city = data.get('city', 'N/A')
|
||||
self.postal_code = data.get('postal_code', 'N/A')
|
||||
|
||||
##USER Space##
|
||||
|
||||
# ##USER Space##
|
||||
|
||||
|
||||
class UserEmail(object):
|
||||
def __init__(self, data=None):
|
||||
data = data or {}
|
||||
self.email = data.get('email', 'N/A')
|
||||
self.user_id = data.get('user_id', 'N/A')
|
||||
self.user_thumb = data.get('user_thumb', 'N/A')
|
||||
|
||||
##API Space##
|
||||
|
||||
|
||||
# ##API Space##
|
||||
|
||||
|
||||
def get_user_ip_addresses(user_id='', ip_address=''):
|
||||
# Get the user IP list from Tautulli
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_user_ips',
|
||||
'user_id': user_id,
|
||||
'search': ip_address}
|
||||
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
@ -99,6 +114,7 @@ def get_user_ip_addresses(user_id='', ip_address=''):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_user_ip_addresses' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_geoip_info(ip_address=''):
|
||||
# Get the geo IP lookup from Tautulli
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -146,6 +162,7 @@ def get_user_email(user_id=''):
|
||||
sys.stderr.write("Tautulli API 'get_user' request failed: {0}.".format(e))
|
||||
return UserEmail()
|
||||
|
||||
|
||||
def send_notification(arguments=None, geodata=None, useremail=None):
|
||||
# Format notification text
|
||||
try:
|
||||
@ -163,12 +180,12 @@ def send_notification(arguments=None, geodata=None, useremail=None):
|
||||
mailserver.login(email_username, email_password)
|
||||
mailserver.sendmail(sender, u.email, message.as_string())
|
||||
mailserver.quit()
|
||||
print 'Email sent'
|
||||
print('Email sent')
|
||||
except Exception as e:
|
||||
sys.stderr.write("Email Failure: {0}.".format(e))
|
||||
|
||||
def clr_sql(ip):
|
||||
|
||||
def clr_sql(ip):
|
||||
try:
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'sql',
|
||||
@ -179,6 +196,7 @@ def clr_sql(ip):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_sql' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Parse arguments from Tautulli
|
||||
parser = argparse.ArgumentParser()
|
||||
@ -188,7 +206,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('-us', '--user', action='store', default='',
|
||||
help='Username of the person watching the stream')
|
||||
parser.add_argument('-uid', '--user_id', action='store', default='',
|
||||
help='User_ID of the person watching the stream')
|
||||
help='User_ID of the person watching the stream')
|
||||
parser.add_argument('-med', '--media_type', action='store', default='',
|
||||
help='The media type of the stream')
|
||||
parser.add_argument('-tt', '--title', action='store', default='',
|
||||
@ -217,7 +235,7 @@ if __name__ == '__main__':
|
||||
help='The summary of the TV show')
|
||||
parser.add_argument('-lbn', '--library_name', action='store', default='',
|
||||
help='The name of the TV show')
|
||||
|
||||
|
||||
p = parser.parse_args()
|
||||
|
||||
if p.user_id not in IGNORE_LST:
|
||||
|
123
notify/top_concurrent_notify.py
Normal file
123
notify/top_concurrent_notify.py
Normal file
@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
Description: Check Tautulli's most concurrent from home stats against current concurrent count.
|
||||
If greater notify using an existing agent.
|
||||
Author: Blacktwin
|
||||
Requires: requests
|
||||
|
||||
Enabling Scripts in Tautulli:
|
||||
Tautulli > Settings > Notification Agents > Add a Notification Agent > Script
|
||||
|
||||
Configuration:
|
||||
Tautulli > Settings > Notification Agents > New Script > Configuration:
|
||||
|
||||
Script Name: Most Concurrent Record
|
||||
Set Script Timeout: {timeout}
|
||||
Description: New Most Concurrent Record
|
||||
Save
|
||||
|
||||
Triggers:
|
||||
Tautulli > Settings > Notification Agents > New Script > Triggers:
|
||||
|
||||
Check: Playback Start
|
||||
Save
|
||||
|
||||
Conditions:
|
||||
Tautulli > Settings > Notification Agents > New Script > Conditions:
|
||||
|
||||
Set Conditions: [{condition} | {operator} | {value} ]
|
||||
Save
|
||||
|
||||
Script Arguments:
|
||||
Tautulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
Select: Playback Start
|
||||
Arguments: --streams {streams} --notifier notifierID
|
||||
|
||||
*notifierID of the existing agent you want to use to send notification.
|
||||
|
||||
|
||||
Save
|
||||
Close
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import argparse
|
||||
|
||||
|
||||
# ### EDIT SETTINGS ###
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
TAUTULLI_URL = os.getenv('TAUTULLI_URL', TAUTULLI_URL)
|
||||
TAUTULLI_APIKEY = os.getenv('TAUTULLI_APIKEY', TAUTULLI_APIKEY)
|
||||
VERIFY_SSL = False
|
||||
|
||||
|
||||
SUBJECT = 'New Record for Most Concurrent Streams!'
|
||||
BODY = 'New server record for most concurrent streams is now {}.'
|
||||
|
||||
# ## CODE BELOW ##
|
||||
|
||||
|
||||
def get_home_stats():
|
||||
# Get the homepage watch statistics.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_home_stats'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
res_data = response['response']['data']
|
||||
most_concurrents = [rows for rows in res_data if rows['stat_id'] == 'most_concurrent']
|
||||
concurrent_rows = most_concurrents[0]['rows']
|
||||
return concurrent_rows
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_home_stats' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def notify(notifier_id, subject, body):
|
||||
"""Call Tautulli's notify api endpoint"""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'notify',
|
||||
'notifier_id': notifier_id,
|
||||
'subject': subject,
|
||||
'body': body}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
res_data = response['response']['data']
|
||||
return res_data
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'notify' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Notification of new most concurrent streams count.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--streams', required=True, type=int,
|
||||
help='Current streams count from Tautulli.')
|
||||
parser.add_argument('--notifier', required=True,
|
||||
help='Tautulli notification ID to send notification to.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
most_concurrent = get_home_stats()
|
||||
for result in most_concurrent:
|
||||
if result['title'] == 'Concurrent Streams':
|
||||
if opts.streams > result['count']:
|
||||
notify(notifier_id=opts.notifier, subject=SUBJECT, body=BODY.format(opts.streams))
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
1. Install the requests module for python.
|
||||
pip install requests
|
||||
@ -13,19 +16,20 @@ Tautulli > Settings > Notifications > Script > Script Arguments:
|
||||
|
||||
https://gist.github.com/blacktwin/261c416dbed08291e6d12f6987d9bafa
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from twitter import *
|
||||
from twitter import Twitter, OAuth
|
||||
import argparse
|
||||
import requests
|
||||
import os
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TOKEN = ''
|
||||
TOKEN_SECRET = ''
|
||||
CONSUMER_KEY = ''
|
||||
CONSUMER_SECRET = ''
|
||||
|
||||
TITLE_FIND = ['Friends'] # Title to ignore ['Snow White']
|
||||
TITLE_FIND = ['Friends'] # Title to ignore ['Snow White']
|
||||
TWITTER_USER = ' @username'
|
||||
|
||||
BODY_TEXT = ''
|
||||
@ -80,12 +84,13 @@ if __name__ == '__main__':
|
||||
|
||||
p = parser.parse_args()
|
||||
|
||||
|
||||
if p.media_type == 'movie':
|
||||
BODY_TEXT = MOVIE_TEXT.format(media_type=p.media_type, title=p.title, duration=p.duration)
|
||||
elif p.media_type == 'episode':
|
||||
BODY_TEXT = TV_TEXT.format(media_type=p.media_type, show_name=p.show_name, title=p.title,
|
||||
season_num00=p.season_num, episode_num00=p.episode_num, duration=p.duration)
|
||||
BODY_TEXT = TV_TEXT.format(
|
||||
media_type=p.media_type, show_name=p.show_name, title=p.title,
|
||||
season_num00=p.season_num, episode_num00=p.episode_num,
|
||||
duration=p.duration)
|
||||
else:
|
||||
exit()
|
||||
|
||||
@ -101,7 +106,6 @@ if __name__ == '__main__':
|
||||
for chunk in request:
|
||||
image.write(chunk)
|
||||
|
||||
|
||||
t_upload = Twitter(domain='upload.twitter.com',
|
||||
auth=OAuth(TOKEN, TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET))
|
||||
|
||||
|
@ -1,30 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Find when media was added between STARTFRAME and ENDFRAME to Plex through Tautulli.
|
||||
|
||||
Some Exceptions have been commented out to supress what is printed.
|
||||
Some Exceptions have been commented out to supress what is printed.
|
||||
Uncomment Exceptions if you run into problem and need to investigate.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import str
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
STARTFRAME = 1480550400 # 2016, Dec 1 in seconds
|
||||
ENDFRAME = 1488326400 # 2017, March 1 in seconds
|
||||
STARTFRAME = 1480550400 # 2016, Dec 1 in seconds
|
||||
ENDFRAME = 1488326400 # 2017, March 1 in seconds
|
||||
|
||||
TODAY = int(time.time())
|
||||
LASTMONTH = int(TODAY - 2629743) # 2629743 = 1 month in seconds
|
||||
LASTMONTH = int(TODAY - 2629743) # 2629743 = 1 month in seconds
|
||||
|
||||
# Uncomment to change range to 1 month ago - Today
|
||||
# STARTFRAME = LASTMONTH
|
||||
# ENDFRAME = TODAY
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
LIBRARY_NAMES = ['TV Shows', 'Movies'] # Names of your libraries you want to check.
|
||||
LIBRARY_NAMES = ['TV Shows', 'Movies'] # Names of your libraries you want to check.
|
||||
|
||||
|
||||
class LIBINFO(object):
|
||||
@ -70,6 +75,7 @@ def get_new_rating_keys(rating_key, media_type):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_new_rating_keys' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_library_media_info(section_id):
|
||||
# Get the data on the Tautulli media info tables. Length matters!
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -88,6 +94,7 @@ def get_library_media_info(section_id):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_metadata(rating_key):
|
||||
# Get the metadata for a media item.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -106,6 +113,7 @@ def get_metadata(rating_key):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def update_library_media_info(section_id):
|
||||
# Get the data on the Tautulli media info tables.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -122,6 +130,7 @@ def update_library_media_info(section_id):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'update_library_media_info' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_libraries_table():
|
||||
# Get the data on the Tautulli libraries table.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -159,10 +168,10 @@ for i in glt:
|
||||
# Find movie rating_key.
|
||||
show_lst += [int(x.rating_key)]
|
||||
except Exception as e:
|
||||
print("Rating_key failed: {e}").format(e=e)
|
||||
print(("Rating_key failed: {e}").format(e=e))
|
||||
|
||||
except Exception as e:
|
||||
print("Library media info failed: {e}").format(e=e)
|
||||
print(("Library media info failed: {e}").format(e=e))
|
||||
|
||||
# All rating_keys for episodes and movies.
|
||||
# Reserving order will put newest rating_keys first
|
||||
@ -181,15 +190,16 @@ for i in sorted(show_lst, reverse=True):
|
||||
# Shows
|
||||
print(u"{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when}.".format(x=x, when=added))
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# Remove commented print below to investigate problems.
|
||||
# print("Metadata failed. Likely end of range: {e}").format(e=e)
|
||||
# Remove break if not finding files in range
|
||||
break
|
||||
|
||||
print("There were {amount} files added between {start}:{end}".format(amount=len(count_lst),
|
||||
start=time.ctime(float(STARTFRAME)),
|
||||
end=time.ctime(float(ENDFRAME))))
|
||||
print("There were {amount} files added between {start}:{end}".format(
|
||||
amount=len(count_lst),
|
||||
start=time.ctime(float(STARTFRAME)),
|
||||
end=time.ctime(float(ENDFRAME))))
|
||||
print("Total movies: {}".format(count_lst.count('movie')))
|
||||
print("Total shows: {}".format(count_lst.count('show') + count_lst.count('episode')))
|
||||
print("Total size of files added: {}MB".format(sum(size_lst)>>20))
|
||||
print("Total size of files added: {}MB".format(sum(size_lst) >> 20))
|
||||
|
@ -1,16 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 1. Install the requests module for python.
|
||||
# pip install requests
|
||||
# 2. Add script arguments in Tautulli.
|
||||
# {user} {title}
|
||||
# Add to Playback Resume
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
|
||||
user = sys.argv[1]
|
||||
title = sys.argv[2]
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXXXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
NOTIFIER_ID = 10 # The notification notifier ID for Tautulli
|
||||
@ -25,7 +30,7 @@ BODY_TEXT = """\
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""" %(user, title)
|
||||
""" % (user, title)
|
||||
|
||||
|
||||
class UserHIS(object):
|
||||
@ -33,9 +38,9 @@ class UserHIS(object):
|
||||
data = data or {}
|
||||
self.watched = [d['watched_status'] for d in data]
|
||||
|
||||
|
||||
|
||||
def get_history():
|
||||
# Get the user IP list from Tautulli
|
||||
"""Get the history from Tautulli."""
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_history',
|
||||
'user': user,
|
||||
|
@ -1,19 +1,26 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Run script by itself. Will look for WARN code followed by /library/metadata/ str in Plex logs.
|
||||
This is find files that are corrupt or having playback issues.
|
||||
I corrupted a file to test.
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
lib_met = []
|
||||
err_title = []
|
||||
|
||||
|
||||
class PlexLOG(object):
|
||||
def __init__(self, data=None):
|
||||
self.error_msg = []
|
||||
@ -43,6 +50,7 @@ def get_plex_log():
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_plex_log' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_history(key):
|
||||
# Get the user IP list from Tautulli
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -59,6 +67,7 @@ def get_history(key):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p_log = get_plex_log()
|
||||
for co, msg in p_log.error_msg:
|
||||
|
@ -1,3 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import psutil
|
||||
import requests
|
||||
|
||||
@ -6,20 +10,21 @@ drive = 'F:'
|
||||
|
||||
disk = psutil.disk_partitions()
|
||||
|
||||
TAUTULLI_URL = 'http://localhost:8182/' # Your Tautulli URL
|
||||
TAUTULLI_APIKEY = 'xxxxxx' # Enter your Tautulli API Key
|
||||
NOTIFIER_LST = [10, 11] # The Tautulli notifier notifier id found here: https://github.com/drzoidberg33/plexpy/blob/master/plexpy/notifiers.py#L43
|
||||
NOTIFY_SUBJECT = 'Tautulli' # The notification subject
|
||||
NOTIFY_BODY = 'The Plex disk {0} was not found'.format(drive) # The notification body
|
||||
TAUTULLI_URL = 'http://localhost:8182/' # Your Tautulli URL
|
||||
TAUTULLI_APIKEY = 'xxxxxx' # Enter your Tautulli API Key
|
||||
NOTIFIER_LST = [10, 11] # The Tautulli notifier notifier id found here: https://github.com/drzoidberg33/plexpy/blob/master/plexpy/notifiers.py#L43
|
||||
NOTIFY_SUBJECT = 'Tautulli' # The notification subject
|
||||
NOTIFY_BODY = 'The Plex disk {0} was not found'.format(drive) # The notification body
|
||||
|
||||
disk_check = [True for i in disk if drive in i.mountpoint]
|
||||
|
||||
if not disk_check:
|
||||
# Send the notification through Tautulli
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'notify',
|
||||
'subject': NOTIFY_SUBJECT,
|
||||
'body': NOTIFY_BODY}
|
||||
payload = {
|
||||
'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'notify',
|
||||
'subject': NOTIFY_SUBJECT,
|
||||
'body': NOTIFY_BODY}
|
||||
|
||||
for notifier in NOTIFIER_LST:
|
||||
payload['notifier_id'] = notifier
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Use Tautulli to print plays by library from 0, 1, 7, or 30 days ago. 0 = total
|
||||
|
||||
@ -21,19 +24,23 @@ Usage:
|
||||
Plays: 56 : 754 : 2899
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import str
|
||||
import requests
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
|
||||
TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
OUTPUT = 'Library: {section}\nDays: {days}\nPlays: {plays}'
|
||||
|
||||
## CODE BELOW ##
|
||||
# ## CODE BELOW ##
|
||||
|
||||
|
||||
def get_library_names():
|
||||
# Get a list of new rating keys for the PMS of all of the item's parent/children.
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Use Tautulli to pull plays by library
|
||||
|
||||
@ -6,30 +9,31 @@ optional arguments:
|
||||
-l [ ...], --libraries [ ...]
|
||||
Space separated list of case sensitive names to process. Allowed names are:
|
||||
(choices: All Library Names)
|
||||
|
||||
|
||||
|
||||
|
||||
Usage:
|
||||
plays_by_library.py -l "TV Shows" Movies
|
||||
TV Shows - Plays: 2859
|
||||
Movies - Plays: 379
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
# import json
|
||||
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
|
||||
TAUTULLI_APIKEY = 'xxxxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
|
||||
OUTPUT = '{section} - Plays: {plays}'
|
||||
|
||||
## CODE BELOW ##
|
||||
# ## CODE BELOW ##
|
||||
|
||||
|
||||
def get_libraries_table(sections=None):
|
||||
# Get a list of new rating keys for the PMS of all of the item's parent/children.
|
||||
|
@ -1,227 +0,0 @@
|
||||
"""
|
||||
usage: plex_netflix_check.py [-h] [-l [...]] [-s ] [-t ]
|
||||
|
||||
Use instantwatcher.com to find if Plex items are on Netflix, Amazon, or both.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-l [ ...], --library [ ...]
|
||||
Space separated list of case sensitive names to process. Allowed names are:
|
||||
(choices: Your show or movie library names)
|
||||
-s [], --search [] Search any name.
|
||||
-t [], --type [] Refine search for name by using type.
|
||||
(choices: movie, show)
|
||||
-e [], --episodes [] Refine search for individual episodes.
|
||||
(choices: True, False)
|
||||
(default: False)
|
||||
-site [], --site [] Refine search for name by using type.
|
||||
(choices: Netflix, Amazon, Both)
|
||||
(default: Both)
|
||||
-sl [], --search_limit []
|
||||
Define number of search returns from page. Zero returns all.
|
||||
(default: 5)
|
||||
|
||||
If title is matched in both, Amazon is first then Netflix.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
from xmljson import badgerfish as bf
|
||||
from lxml.html import fromstring
|
||||
from time import sleep
|
||||
import json
|
||||
from plexapi.server import PlexServer
|
||||
# pip install plexapi
|
||||
|
||||
|
||||
## Edit ##
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxx'
|
||||
## /Edit ##
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
|
||||
def instantwatch_search(name, media_type, site, search_limit):
|
||||
|
||||
NETFLIX_URL = 'http://www.netflix.com/title/'
|
||||
limit = False
|
||||
results_count = 0
|
||||
|
||||
if media_type == 'movie':
|
||||
content_type = '1'
|
||||
elif media_type == 'show':
|
||||
content_type = '2'
|
||||
elif media_type == 'episode':
|
||||
content_type = '4'
|
||||
else:
|
||||
content_type =''
|
||||
|
||||
payload = {'content_type': content_type,
|
||||
'q': name.lower()}
|
||||
|
||||
if site == 'Netflix':
|
||||
r = requests.get('http://instantwatcher.com/search'.rstrip('/'), params=payload)
|
||||
elif site == 'Amazon':
|
||||
r = requests.get('http://instantwatcher.com/a/search'.rstrip('/'), params=payload)
|
||||
else:
|
||||
r = requests.get('http://instantwatcher.com/u/search'.rstrip('/'), params=payload)
|
||||
|
||||
results_lst = []
|
||||
res_data = bf.data(fromstring(r.content))
|
||||
|
||||
res_data = res_data['html']['body']['div']['div'][1]
|
||||
|
||||
# Any matches?
|
||||
res_results = res_data['div'][0]['div'][1]['div'][0]
|
||||
title_check = res_data['div'][0]['div'][1]['div'][1]
|
||||
|
||||
try:
|
||||
if res_results['span']:
|
||||
total_results = res_results['span']
|
||||
for data in total_results:
|
||||
results_lst += [data['$']]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print('{} found {}.'.format(results_lst[0], results_lst[1]))
|
||||
result_count = int(results_lst[1].split(' ')[0])
|
||||
|
||||
amazon_id = ''
|
||||
amazon_url = ''
|
||||
|
||||
# Title match
|
||||
if result_count == 0:
|
||||
print('0 matches, moving on.')
|
||||
pass
|
||||
else:
|
||||
item_results_page = title_check['div']['div']
|
||||
if result_count > 1:
|
||||
for results in item_results_page:
|
||||
for data in results['a']:
|
||||
try:
|
||||
amazon_id = data['@data-amazon-title-id']
|
||||
amazon_url = data['@data-amazon-uri']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for data in results['span']:
|
||||
if data['@class'] == 'title' and search_limit is not 0:
|
||||
if str(data['a']['$']).lower().startswith(name.lower()):
|
||||
if amazon_id:
|
||||
if data['a']['@data-title-id'] == amazon_id:
|
||||
print('Match found on Amazon for {}'.format(data['a']['$']))
|
||||
print('Page: {}'.format(amazon_url))
|
||||
else:
|
||||
print('Match found on Netflix for {}'.format(data['a']['$']))
|
||||
print('Page: {}{}'.format(NETFLIX_URL, data['a']['@data-title-id']))
|
||||
results_count += 1
|
||||
search_limit -= 1
|
||||
if search_limit is 0:
|
||||
limit = True
|
||||
|
||||
elif data['@class'] == 'title' and search_limit is 0 and limit is False:
|
||||
if data['a']['$'].lower().startswith(name.lower()):
|
||||
if amazon_id:
|
||||
if data['a']['@data-title-id'] == amazon_id:
|
||||
print('Match found on Amazon for {}'.format(data['a']['$']))
|
||||
print('Page: {}'.format(amazon_url))
|
||||
else:
|
||||
print('Match found on Netflix for {}'.format(data['a']['$']))
|
||||
print('Page: {}{}'.format(NETFLIX_URL, data['a']['@data-title-id']))
|
||||
results_count += 1
|
||||
|
||||
elif result_count == 1:
|
||||
for data in item_results_page['a']:
|
||||
try:
|
||||
amazon_id = data['@data-amazon-title-id']
|
||||
amazon_url = data['@data-amazon-uri']
|
||||
except Exception:
|
||||
pass
|
||||
for data in item_results_page['span']:
|
||||
if data['@class'] == 'title':
|
||||
if data['a']['$'].lower().startswith(name.lower()):
|
||||
print('Match! For {}'.format(data['a']['$']))
|
||||
if amazon_id:
|
||||
if data['a']['@data-title-id'] == amazon_id:
|
||||
print('Page: {}'.format(amazon_url))
|
||||
else:
|
||||
print('Page: {}{}'.format(NETFLIX_URL, data['a']['@data-title-id']))
|
||||
results_count += 1
|
||||
else:
|
||||
print('Could not find exact name match.')
|
||||
return results_count
|
||||
|
||||
|
||||
def plex_library_search(lib_name, site, epi_search, search_limit):
|
||||
for title in plex.library.section(lib_name).all():
|
||||
print('Running check on {}'.format(title.title))
|
||||
file_path = []
|
||||
if title.type == 'show' and epi_search is True:
|
||||
if instantwatch_search(title.title, title.type, site, search_limit) > 0:
|
||||
print('Show was found. Searching for episode paths.')
|
||||
for episode in title.episodes():
|
||||
# Need to check episodes against sites to truly find episode matches.
|
||||
# For now just return paths for episodes if Show name matches.
|
||||
# print('Running check on {} - {}'.format(title.title, episode.title))
|
||||
# show_ep = '{} - {}'.format(title.title, episode.title)
|
||||
# if instantwatch_search(show_ep, 'episode', site) > 0:
|
||||
file_path += [episode.media[0].parts[0].file]
|
||||
|
||||
elif title.type == 'movie':
|
||||
if instantwatch_search(title.title, title.type, site, search_limit) > 0:
|
||||
file_path = title.media[0].parts[0].file
|
||||
else:
|
||||
if instantwatch_search(title.title, title.type, site, search_limit) > 0:
|
||||
print('Show was found but path is not defined.')
|
||||
|
||||
if file_path:
|
||||
if type(file_path) is str:
|
||||
print('File: {}'.format(file_path))
|
||||
elif type(file_path) is list:
|
||||
print('Files: \n{}'.format(' \n'.join(file_path)))
|
||||
|
||||
print('Waiting 5 seconds before next search.')
|
||||
sleep(5)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
sections_lst = [d.title for d in plex.library.sections() if d.type in ['show', 'movie']]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Use instantwatcher.com to find if Plex items are on Netflix.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('-l', '--library', metavar='', choices=sections_lst, nargs='+',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are:\n'
|
||||
'(choices: %(choices)s)')
|
||||
parser.add_argument('-s', '--search', metavar='', nargs='?', type=str,
|
||||
help='Search any name.')
|
||||
parser.add_argument('-m', '--media_type', metavar='', choices=['movie', 'show'], nargs='?',
|
||||
help='Refine search for name by using media type.\n'
|
||||
'(choices: %(choices)s)')
|
||||
parser.add_argument('-e', '--episodes', metavar='', nargs='?', type=bool, default=False, choices=[True, False],
|
||||
help='Refine search for individual episodes.\n'
|
||||
'(choices: %(choices)s)\n(default: %(default)s)')
|
||||
parser.add_argument('-site', '--site', metavar='', choices=['Netflix', 'Amazon', 'Both'], nargs='?',
|
||||
default='Both', help='Refine search for name by using type.\n'
|
||||
'(choices: %(choices)s)\n(default: %(default)s)')
|
||||
parser.add_argument('-sl', '--search_limit', metavar='', nargs='?', type=int, default=5,
|
||||
help='Define number of search returns from page. Zero returns all.'
|
||||
'\n(default: %(default)s)')
|
||||
|
||||
opts = parser.parse_args()
|
||||
# print(opts)
|
||||
|
||||
if opts.search:
|
||||
instantwatch_search(opts.search, opts.media_type, opts.site, opts.search_limit)
|
||||
else:
|
||||
if len(opts.library) > 1:
|
||||
for section in opts.library:
|
||||
plex_library_search(section, opts.site, opts.episodes, opts.search_limit)
|
||||
else:
|
||||
plex_library_search(opts.library[0], opts.site, opts.episodes, opts.search_limit)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Comparing content between two or more Plex servers.
|
||||
Creates .json file in script directory of server compared.
|
||||
@ -15,6 +17,8 @@ Requires: requests, plexapi
|
||||
python find_diff_other_servers.py --server "My Plex Server" --server PlexServer2 --server "Steven Plex"
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
@ -101,12 +105,16 @@ def get_meta(meta):
|
||||
"title": "Title"
|
||||
}
|
||||
"""
|
||||
thumb_url = '{}{}?X-Plex-Token={}'.format(
|
||||
meta._server._baseurl, meta.thumb, meta._server._token)
|
||||
|
||||
meta_dict = {'title': meta.title,
|
||||
'rating': meta.rating,
|
||||
'rating': meta.rating if
|
||||
meta.rating is not None else 0.0,
|
||||
'genres': [x.tag for x in meta.genres],
|
||||
'server': [meta._server.friendlyName]
|
||||
}
|
||||
'server': [meta._server.friendlyName],
|
||||
'thumb': [thumb_url]
|
||||
}
|
||||
if meta.guid:
|
||||
# guid will return (com.plexapp.agents.imdb://tt4302938?lang=en)
|
||||
# Agents will differ between servers.
|
||||
@ -121,7 +129,7 @@ def get_meta(meta):
|
||||
return meta_dict
|
||||
|
||||
|
||||
def org_diff(media_type, lst_dicts, main_server):
|
||||
def org_diff(lst_dicts, media_type, main_server):
|
||||
"""Organizing the items from each server
|
||||
|
||||
Parameters
|
||||
@ -153,13 +161,13 @@ def org_diff(media_type, lst_dicts, main_server):
|
||||
}
|
||||
"""
|
||||
diff_dict = {}
|
||||
seen = {}
|
||||
dupes = []
|
||||
missing = []
|
||||
unique = []
|
||||
# todo-me pull posters from connected servers
|
||||
|
||||
for mtype in media_type:
|
||||
meta_lst = []
|
||||
seen = {}
|
||||
missing = []
|
||||
unique = []
|
||||
print('...combining {}s'.format(mtype))
|
||||
for server_lst in lst_dicts:
|
||||
for item in server_lst[mtype]:
|
||||
@ -167,6 +175,7 @@ def org_diff(media_type, lst_dicts, main_server):
|
||||
title = u'{} ({})'.format(item.title, item.year)
|
||||
else:
|
||||
title = item.title
|
||||
|
||||
# Look for duplicate titles
|
||||
if title not in seen:
|
||||
seen[title] = 1
|
||||
@ -174,19 +183,21 @@ def org_diff(media_type, lst_dicts, main_server):
|
||||
else:
|
||||
# Duplicate found
|
||||
if seen[title] >= 1:
|
||||
dupes.append([title,item._server.friendlyName])
|
||||
# Go back through list to find original
|
||||
for meta in meta_lst:
|
||||
if meta['title'] == title:
|
||||
# Append the duplicate server's name
|
||||
meta['server'].append(item._server.friendlyName)
|
||||
thumb_url = '{}{}?X-Plex-Token={}'.format(
|
||||
item._server._baseurl, item.thumb, item._server._token)
|
||||
meta['thumb'].append(thumb_url)
|
||||
seen[title] += 1
|
||||
# Sort item list by Plex rating
|
||||
# Duplicates will use originals rating
|
||||
meta_lst = sorted(meta_lst, key=lambda d: d['rating'],
|
||||
reverse=True)
|
||||
diff_dict[mtype] = {'combined': {'count': len(meta_lst),
|
||||
'list': meta_lst}}
|
||||
meta_lst = sorted(meta_lst, key=lambda d: d['rating'], reverse=True)
|
||||
diff_dict[mtype] = {'combined': {
|
||||
'count': len(meta_lst),
|
||||
'list': meta_lst}}
|
||||
|
||||
print('...finding {}s missing from {}'.format(
|
||||
mtype, main_server))
|
||||
@ -197,13 +208,15 @@ def org_diff(media_type, lst_dicts, main_server):
|
||||
# Main Server name is absent in items server list
|
||||
elif main_server in item['server'] and len(item['server']) == 1:
|
||||
unique.append(item)
|
||||
diff_dict[mtype].update({'missing': {'count': len(missing),
|
||||
'list': missing}})
|
||||
diff_dict[mtype].update({'missing': {
|
||||
'count': len(missing),
|
||||
'list': missing}})
|
||||
|
||||
print('...finding {}s unique to {}'.format(
|
||||
mtype, main_server))
|
||||
diff_dict[mtype].update({'unique': {'count': len(unique),
|
||||
'list': unique}})
|
||||
diff_dict[mtype].update({'unique': {
|
||||
'count': len(unique),
|
||||
'list': unique}})
|
||||
|
||||
return diff_dict
|
||||
|
||||
@ -212,7 +225,7 @@ if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Comparing content between two or more Plex servers.",
|
||||
formatter_class = argparse.RawTextHelpFormatter)
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--server', required=True, choices=SERVER_DICT.keys(),
|
||||
action='append', nargs='?', metavar='',
|
||||
help='Choose servers to connect to and compare.\n'
|
||||
@ -264,7 +277,8 @@ if __name__ == "__main__":
|
||||
|
||||
main_dict = org_diff(combined_lst, opts.media_type, main_server.friendlyName)
|
||||
|
||||
filename = 'diff_{}_{}_servers.json'.format(opts.server[0],'_'.join(servers))
|
||||
filename = 'diff_{}_{}_servers.json'.format(
|
||||
opts.server[0], '_'.join(servers))
|
||||
|
||||
with open(filename, 'w') as fp:
|
||||
json.dump(main_dict, fp, indent=4, sort_keys=True)
|
||||
|
100
reporting/streaming_service_availability.py
Normal file
100
reporting/streaming_service_availability.py
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Check media availability on streaming services.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from plexapi import CONFIG
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.exceptions import BadRequest
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Environment Variables or PlexAPI Config
|
||||
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) or CONFIG.data['auth'].get('server_baseurl')
|
||||
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) or CONFIG.data['auth'].get('server_token')
|
||||
|
||||
|
||||
def check_streaming_services(plex, libraries, services, available_only):
|
||||
if libraries:
|
||||
sections = [plex.library.section(library) for library in libraries]
|
||||
else:
|
||||
sections = [
|
||||
section for section in plex.library.sections()
|
||||
if section.agent in {'tv.plex.agents.movie', 'tv.plex.agents.series'}
|
||||
]
|
||||
|
||||
for section in sections:
|
||||
print(f'{section.title}')
|
||||
|
||||
for item in section.all():
|
||||
try:
|
||||
availabilities = item.streamingServices()
|
||||
except BadRequest:
|
||||
continue
|
||||
|
||||
if services:
|
||||
availabilities = [
|
||||
availability for availability in availabilities
|
||||
if availability.title in services
|
||||
]
|
||||
|
||||
if available_only and not availabilities:
|
||||
continue
|
||||
|
||||
if item.type == 'movie':
|
||||
subtitle = item.media[0].videoResolution
|
||||
subtitle = subtitle.upper() if subtitle == 'sd' else ((subtitle + 'p') if subtitle.isdigit() else '')
|
||||
else:
|
||||
subtitle = item.childCount
|
||||
subtitle = str(subtitle) + ' season' + ('s' if subtitle > 1 else '')
|
||||
|
||||
print(f' └─ {item.title} ({item.year}) ({subtitle})')
|
||||
|
||||
for availability in availabilities:
|
||||
title = availability.title
|
||||
quality = availability.quality
|
||||
offerType = availability.offerType.capitalize()
|
||||
priceDescription = (' ' + availability.priceDescription) if availability.priceDescription else ''
|
||||
print(f' └─ {title} ({quality} - {offerType}{priceDescription})')
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'--libraries',
|
||||
'-l',
|
||||
nargs='+',
|
||||
help=(
|
||||
'Plex libraries to check (e.g. Movies, TV Shows, etc.). '
|
||||
'Default: All movie and tv show libraries using the Plex Movie or Plex TV Series agents.'
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--services',
|
||||
'-s',
|
||||
nargs='+',
|
||||
help=(
|
||||
'Streaming services to check (e.g. Netflix, Disney+, Amazon Prime Video, etc.). '
|
||||
'Note: Must be the exact name of the service as it appears in Plex. '
|
||||
'Default: All services.'
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'--available_only',
|
||||
'-a',
|
||||
action='store_true',
|
||||
help=(
|
||||
'Only list media that is available on at least one streaming service.'
|
||||
)
|
||||
)
|
||||
opts = parser.parse_args()
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
check_streaming_services(plex, **vars(opts))
|
@ -1,8 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Use Tautulli to count how many plays per user occurred this week.
|
||||
Notify via Tautulli Notification
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
@ -10,7 +15,7 @@ import time
|
||||
TODAY = int(time.time())
|
||||
LASTWEEK = int(TODAY - 7 * 24 * 60 * 60)
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
SUBJECT_TEXT = "Tautulli Weekly Plays Per User"
|
||||
@ -29,12 +34,13 @@ class UserHIS(object):
|
||||
self.full_title = d['full_title']
|
||||
self.date = d['date']
|
||||
|
||||
|
||||
def get_history():
|
||||
# Get the Tautulli history. Count matters!!!
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_history',
|
||||
'length': 100000}
|
||||
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
@ -42,10 +48,11 @@ def get_history():
|
||||
res_data = response['response']['data']['data']
|
||||
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1 and
|
||||
LASTWEEK < d['date'] < TODAY]
|
||||
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def send_notification(BODY_TEXT):
|
||||
# Format notification text
|
||||
try:
|
||||
@ -73,13 +80,15 @@ def send_notification(BODY_TEXT):
|
||||
sys.stderr.write("Tautulli API 'notify' request failed: {0}.".format(e))
|
||||
return None
|
||||
|
||||
|
||||
def add_to_dictlist(d, key, val):
|
||||
if key not in d:
|
||||
d[key] = [val]
|
||||
else:
|
||||
d[key].append(val)
|
||||
|
||||
user_dict ={}
|
||||
|
||||
user_dict = {}
|
||||
notify_lst = []
|
||||
|
||||
[add_to_dictlist(user_dict, h.user, h.media) for h in get_history()]
|
||||
@ -106,7 +115,9 @@ BODY_TEXT = """\
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""".format(notify_lst="\n".join(notify_lst).encode("utf-8"),end=time.ctime(float(TODAY)),
|
||||
start=time.ctime(float(LASTWEEK)))
|
||||
""".format(
|
||||
notify_lst="\n".join(notify_lst).encode("utf-8"),
|
||||
end=time.ctime(float(TODAY)),
|
||||
start=time.ctime(float(LASTWEEK)))
|
||||
|
||||
send_notification(BODY_TEXT)
|
||||
|
468
reporting/watched_percentages.py
Normal file
468
reporting/watched_percentages.py
Normal file
@ -0,0 +1,468 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from builtins import object
|
||||
import time
|
||||
import argparse
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import CONFIG
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Using CONFIG file
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
|
||||
FONT_COLOR = '#FFFFFF'
|
||||
BACKGROUND_COLOR = '#282828'
|
||||
BOX_COLOR = '#3C3C3C'
|
||||
BBOX_PROPS = dict(boxstyle="round,pad=0.7, rounding_size=0.3", fc=BOX_COLOR, ec=BOX_COLOR)
|
||||
|
||||
# [user, section] for Explode and Color
|
||||
EXPLODE = [0, 0.01]
|
||||
COLORS = ['#F6A821', '#C07D37']
|
||||
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl')
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
|
||||
if not TAUTULLI_URL:
|
||||
TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
if not TAUTULLI_APIKEY:
|
||||
TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')
|
||||
|
||||
VERIFY_SSL = False
|
||||
|
||||
timestr = time.strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, url=None, apikey=None, verify_ssl=False):
|
||||
self.url = url
|
||||
self.apikey = apikey
|
||||
self.session = Session()
|
||||
self.adapters = HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session.mount('http://', self.adapters)
|
||||
self.session.mount('https://', self.adapters)
|
||||
|
||||
# Ignore verifying the SSL certificate
|
||||
if verify_ssl is False:
|
||||
self.session.verify = False
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
class Library(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.title = d['section_name']
|
||||
self.key = d['section_id']
|
||||
self.count = d['count']
|
||||
self.type = d['section_type']
|
||||
try:
|
||||
self.parent_count = d['parent_count']
|
||||
self.child_count = d['child_count']
|
||||
except Exception:
|
||||
# print(e)
|
||||
pass
|
||||
|
||||
|
||||
class Tautulli(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def _call_api(self, cmd, payload, method='GET'):
|
||||
payload['cmd'] = cmd
|
||||
payload['apikey'] = self.connection.apikey
|
||||
|
||||
try:
|
||||
response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload)
|
||||
except RequestException as e:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
|
||||
return
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
except ValueError:
|
||||
print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd))
|
||||
return
|
||||
|
||||
if response_json['response']['result'] == 'success':
|
||||
return response_json['response']['data']
|
||||
else:
|
||||
error_msg = response_json['response']['message']
|
||||
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
|
||||
return
|
||||
|
||||
def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None):
|
||||
"""Call Tautulli's get_history api endpoint"""
|
||||
payload = {"order_column": "full_title",
|
||||
"order_dir": "asc"}
|
||||
if user:
|
||||
payload["user"] = user
|
||||
if section_id:
|
||||
payload["section_id"] = section_id
|
||||
if rating_key:
|
||||
payload["rating_key"] = rating_key
|
||||
if start:
|
||||
payload["start"] = start
|
||||
if length:
|
||||
payload["lengh"] = length
|
||||
|
||||
history = self._call_api('get_history', payload)
|
||||
|
||||
return [d for d in history['data'] if d['watched_status'] == 1]
|
||||
|
||||
def get_libraries(self):
|
||||
"""Call Tautulli's get_libraries api endpoint"""
|
||||
|
||||
payload = {}
|
||||
return self._call_api('get_libraries', payload)
|
||||
|
||||
|
||||
class Plex(object):
|
||||
def __init__(self, token, url=None):
|
||||
if token and not url:
|
||||
self.account = MyPlexAccount(token)
|
||||
if token and url:
|
||||
session = Connection().session
|
||||
self.server = PlexServer(baseurl=url, token=token, session=session)
|
||||
|
||||
def all_users(self):
|
||||
"""All users
|
||||
Returns
|
||||
-------
|
||||
data: dict
|
||||
"""
|
||||
users = {self.account.title: self.account}
|
||||
for user in self.account.users():
|
||||
users[user.title] = user
|
||||
|
||||
return users
|
||||
|
||||
def all_sections(self):
|
||||
"""All sections from server
|
||||
Returns
|
||||
-------
|
||||
sections: dict
|
||||
{section title: section object}
|
||||
"""
|
||||
sections = {section.title: section for section in self.server.library.sections()}
|
||||
|
||||
return sections
|
||||
|
||||
def all_collections(self):
|
||||
"""All collections from server
|
||||
Returns
|
||||
-------
|
||||
collections: dict
|
||||
{collection title: collection object}
|
||||
"""
|
||||
collections = {}
|
||||
for section in self.all_sections().values():
|
||||
if section.type != 'photo':
|
||||
for collection in section.collections():
|
||||
collections[collection.title] = collection
|
||||
|
||||
return collections
|
||||
|
||||
def all_shows(self):
|
||||
"""All collections from server
|
||||
Returns
|
||||
-------
|
||||
shows: dict
|
||||
{Show title: show object}
|
||||
"""
|
||||
shows = {}
|
||||
for section in self.all_sections().values():
|
||||
if section.type == 'show':
|
||||
for show in section.all():
|
||||
shows[show.title] = show
|
||||
|
||||
return shows
|
||||
|
||||
def all_sections_totals(self, library=None):
|
||||
"""All sections total items
|
||||
Returns
|
||||
-------
|
||||
section_totals: dict or int
|
||||
{section title: section object} or int
|
||||
"""
|
||||
section_totals = {}
|
||||
if library:
|
||||
sections = [self.all_sections()[library]]
|
||||
else:
|
||||
sections = self.all_sections()
|
||||
for section in sections:
|
||||
if section.type == 'movie':
|
||||
section_total = len(section.all())
|
||||
elif section.type == 'show':
|
||||
section_total = len(section.search(libtype='episode'))
|
||||
else:
|
||||
continue
|
||||
|
||||
if library:
|
||||
return section_total
|
||||
|
||||
section_totals[section.title] = section_total
|
||||
|
||||
return section_totals
|
||||
|
||||
|
||||
def make_pie(user_dict, source_dict, title, filename=None, image=None, headless=None):
|
||||
|
||||
import matplotlib as mpl
|
||||
mpl.rcParams['text.color'] = FONT_COLOR
|
||||
mpl.rcParams['axes.labelcolor'] = FONT_COLOR
|
||||
mpl.rcParams['xtick.color'] = FONT_COLOR
|
||||
mpl.rcParams['ytick.color'] = FONT_COLOR
|
||||
if headless:
|
||||
mpl.use("Agg")
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
user_len = len(user_dict.keys())
|
||||
source_len = len(source_dict.keys())
|
||||
user_position = 0
|
||||
|
||||
fig = plt.figure(figsize=(source_len + 10, user_len + 10), facecolor=BACKGROUND_COLOR)
|
||||
|
||||
for user, values in user_dict.items():
|
||||
source_position = 0
|
||||
for source, watched_value in values.items():
|
||||
source_total = source_dict.get(source)
|
||||
percent_watched = 100 * (float(watched_value) / float(source_total))
|
||||
fracs = [percent_watched, 100 - percent_watched]
|
||||
ax = plt.subplot2grid((user_len, source_len), (user_position, source_position))
|
||||
pie, text, autotext = ax.pie(fracs, explode=EXPLODE, colors=COLORS, pctdistance=1.3,
|
||||
autopct='%1.1f%%', shadow=True, startangle=300, radius=0.8,
|
||||
wedgeprops=dict(width=0.5, edgecolor=BACKGROUND_COLOR))
|
||||
|
||||
if user_position == 0:
|
||||
ax.set_title("{}: {}".format(source, source_total), bbox=BBOX_PROPS,
|
||||
ha='center', va='bottom', size=12)
|
||||
if source_position == 0:
|
||||
ax.set_ylabel(user, bbox=BBOX_PROPS, size=13, horizontalalignment='right').set_rotation(0)
|
||||
ax.yaxis.labelpad = 40
|
||||
ax.set_xlabel("User watched: {}".format(watched_value), bbox=BBOX_PROPS)
|
||||
source_position += 1
|
||||
user_position += 1
|
||||
|
||||
plt.suptitle(title, bbox=BBOX_PROPS, size=15)
|
||||
plt.tight_layout()
|
||||
fig.subplots_adjust(top=0.88)
|
||||
|
||||
if filename:
|
||||
plt.savefig('{}_{}.png'.format(filename, timestr), facecolor=BACKGROUND_COLOR)
|
||||
print('Image saved as: {}_{}.png'.format(filename, timestr))
|
||||
if not headless:
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Show watched percentage of users by libraries.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
|
||||
servers = parser.add_mutually_exclusive_group()
|
||||
servers.add_argument('--plex', default=False, action='store_true',
|
||||
help='Pull data from Plex')
|
||||
servers.add_argument('--tautulli', default=False, action='store_true',
|
||||
help='Pull data from Tautulli')
|
||||
|
||||
parser.add_argument('--libraries', nargs='*', metavar='library',
|
||||
help='Libraries to scan for watched content.')
|
||||
parser.add_argument('--collections', nargs='*', metavar='collection',
|
||||
help='Collections to scan for watched content.')
|
||||
parser.add_argument('--shows', nargs='*', metavar='show',
|
||||
help='Shows to scan for watched content.')
|
||||
parser.add_argument('--users', nargs='*', metavar='users',
|
||||
help='Users to scan for watched content.')
|
||||
parser.add_argument('--pie', default=False, action='store_true',
|
||||
help='Display pie chart')
|
||||
parser.add_argument('--filename', type=str, default='Users_Watched_{}'.format(timestr), metavar='',
|
||||
help='Filename of pie chart. None will not save. \n(default: %(default)s)')
|
||||
parser.add_argument('--headless', action='store_true', help='Run headless.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
source_dict = {}
|
||||
user_servers = []
|
||||
sections_dict = {}
|
||||
user_dict = {}
|
||||
title = ''
|
||||
image = ''
|
||||
|
||||
|
||||
if opts.plex:
|
||||
admin_account = Plex(PLEX_TOKEN)
|
||||
plex_server = Plex(PLEX_TOKEN, PLEX_URL)
|
||||
for user in opts.users:
|
||||
user_server = plex_server.server.switchUser(user)
|
||||
user_server._username = user
|
||||
user_servers.append(user_server)
|
||||
|
||||
if opts.libraries:
|
||||
title = "User's Watch Percentage by Library\nFrom: {}"
|
||||
title = title.format(plex_server.server.friendlyName)
|
||||
|
||||
for library in opts.libraries:
|
||||
section_total = plex_server.all_sections_totals(library)
|
||||
source_dict[library] = section_total
|
||||
print("Section: {}, has {} items.".format(library, section_total))
|
||||
for user_server in user_servers:
|
||||
section = user_server.library.section(library)
|
||||
if section.type == 'movie':
|
||||
section_watched_lst = section.search(unwatched=False)
|
||||
elif section.type == 'show':
|
||||
section_watched_lst = section.search(libtype='episode', unwatched=False)
|
||||
else:
|
||||
continue
|
||||
section_watched_total = len(section_watched_lst)
|
||||
percent_watched = 100 * (float(section_watched_total) / float(section_total))
|
||||
print(" {} has watched {} items ({}%).".format(user_server._username, section_watched_total,
|
||||
int(percent_watched)))
|
||||
|
||||
if user_dict.get(user_server._username):
|
||||
user_dict[user_server._username].update({library: section_watched_total})
|
||||
else:
|
||||
user_dict[user_server._username] = {library: section_watched_total}
|
||||
|
||||
if opts.collections:
|
||||
title = "User's Watch Percentage by Collection\nFrom: {}"
|
||||
title = title.format(plex_server.server.friendlyName)
|
||||
for collection in opts.collections:
|
||||
_collection = plex_server.all_collections()[collection]
|
||||
collection_albums = _collection.items()
|
||||
collection_total = len(collection_albums)
|
||||
source_dict[collection] = collection_total
|
||||
print("Collection: {}, has {} items.".format(collection, collection_total))
|
||||
if _collection.subtype == 'album':
|
||||
for user_server in user_servers:
|
||||
collection_watched_lst = []
|
||||
user_collection = user_server.fetchItem(_collection.ratingKey)
|
||||
for album in user_collection.items():
|
||||
if album.viewedLeafCount:
|
||||
collection_watched_lst.append(album)
|
||||
collection_watched_total = len(collection_watched_lst)
|
||||
percent_watched = 100 * (float(collection_watched_total) / float(collection_total))
|
||||
print(" {} has listened {} items ({}%).".format(user_server._username,
|
||||
collection_watched_total,
|
||||
int(percent_watched)))
|
||||
if user_dict.get(user_server._username):
|
||||
user_dict[user_server._username].update({collection: collection_watched_total})
|
||||
else:
|
||||
user_dict[user_server._username] = {collection: collection_watched_total}
|
||||
|
||||
else:
|
||||
collection_items = _collection.items()
|
||||
collection_total = len(collection_items)
|
||||
thumb_url = '{}{}&X-Plex-Token={}'.format(PLEX_URL, _collection.thumb, PLEX_TOKEN)
|
||||
# image = rget(thumb_url, stream=True)
|
||||
image = urllib.request.urlretrieve(thumb_url)
|
||||
for user_server in user_servers:
|
||||
collection_watched_lst = []
|
||||
for item in collection_items:
|
||||
user_item = user_server.fetchItem(item.ratingKey)
|
||||
if user_item.isWatched:
|
||||
collection_watched_lst.append(user_item)
|
||||
collection_watched_total = len(collection_watched_lst)
|
||||
percent_watched = 100 * (float(collection_watched_total) / float(collection_total))
|
||||
print(" {} has watched {} items ({}%).".format(user_server._username,
|
||||
collection_watched_total,
|
||||
int(percent_watched)))
|
||||
if user_dict.get(user_server._username):
|
||||
user_dict[user_server._username].update({collection: collection_watched_total})
|
||||
else:
|
||||
user_dict[user_server._username] = {collection: collection_watched_total}
|
||||
|
||||
if opts.shows:
|
||||
title = "User's Watch Percentage by Shows\nFrom: {}"
|
||||
title = title.format(plex_server.server.friendlyName)
|
||||
all_shows = plex_server.all_shows()
|
||||
|
||||
for show_title in opts.shows:
|
||||
show = all_shows.get(show_title)
|
||||
episode_total = len(show.episodes())
|
||||
season_total = len(show.seasons())
|
||||
source_dict[show_title] = len(show.episodes())
|
||||
print("Show: {}, has {} episodes.".format(show_title, episode_total))
|
||||
for user_server in user_servers:
|
||||
user_show = user_server.fetchItem(show.ratingKey)
|
||||
source_watched_lst = user_show.watched()
|
||||
source_watched_total = len(source_watched_lst)
|
||||
percent_watched = 100 * (float(source_watched_total) / float(episode_total))
|
||||
print(" {} has watched {} items ({}%).".format(user_server._username, source_watched_total,
|
||||
int(percent_watched)))
|
||||
|
||||
if user_dict.get(user_server._username):
|
||||
user_dict[user_server._username].update({show_title: source_watched_total})
|
||||
else:
|
||||
user_dict[user_server._username] = {show_title: source_watched_total}
|
||||
|
||||
elif opts.tautulli:
|
||||
# Create a Tautulli instance
|
||||
tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'),
|
||||
apikey=TAUTULLI_APIKEY,
|
||||
verify_ssl=VERIFY_SSL))
|
||||
# Pull all libraries from Tautulli
|
||||
tautulli_sections = tautulli_server.get_libraries()
|
||||
title = "User's Watch Percentage by Library\nFrom: Tautulli"
|
||||
for section in tautulli_sections:
|
||||
library = Library(section)
|
||||
sections_dict[library.title] = library
|
||||
|
||||
for library in opts.libraries:
|
||||
section = sections_dict[library]
|
||||
if section.type == "movie":
|
||||
section_total = section.count
|
||||
elif section.type == "show":
|
||||
section_total = section.child_count
|
||||
else:
|
||||
print("Not doing that...")
|
||||
section_total = 0
|
||||
|
||||
print("Section: {}, has {} items.".format(library, section_total))
|
||||
source_dict[library] = section_total
|
||||
for user in opts.users:
|
||||
count = 25
|
||||
start = 0
|
||||
section_watched_lst = []
|
||||
try:
|
||||
while True:
|
||||
# Getting all watched history for userFrom
|
||||
tt_watched = tautulli_server.get_watched_history(user=user, section_id=section.key,
|
||||
start=start, length=count)
|
||||
if all([tt_watched]):
|
||||
start += count
|
||||
for item in tt_watched:
|
||||
section_watched_lst.append(item["rating_key"])
|
||||
continue
|
||||
elif not all([tt_watched]):
|
||||
break
|
||||
start += count
|
||||
|
||||
except Exception as e:
|
||||
print((user, e))
|
||||
|
||||
section_watched_total = len(list(set(section_watched_lst)))
|
||||
percent_watched = 100 * (float(section_watched_total) / float(section_total))
|
||||
print(" {} has watched {} items ({}%).".format(user, section_watched_total, int(percent_watched)))
|
||||
|
||||
if user_dict.get(user):
|
||||
user_dict[user].update({library: section_watched_total})
|
||||
else:
|
||||
user_dict[user] = {library: section_watched_total}
|
||||
|
||||
if opts.pie:
|
||||
make_pie(user_dict, source_dict, title, opts.filename, image, opts.headless)
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Pull library and user statistics of last week.
|
||||
|
||||
@ -8,23 +11,56 @@ User stats display username and hour, minutes, and seconds of view time
|
||||
Tautulli Settings > Extra Settings > Check - Calculate Total File Sizes [experimental] ...... wait
|
||||
|
||||
"""
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
import json
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from builtins import range
|
||||
from builtins import object
|
||||
from plexapi.server import CONFIG
|
||||
from datetime import datetime, timedelta, date
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import RequestException
|
||||
from operator import itemgetter
|
||||
import time
|
||||
import json
|
||||
import argparse
|
||||
|
||||
|
||||
# EDIT THESE SETTINGS #
|
||||
TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
SUBJECT_TEXT = "Tautulli Weekly Server, Library, and User Statistics"
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
TAUTULLI_PUBLIC_URL = '0'
|
||||
|
||||
if not TAUTULLI_URL:
|
||||
TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
if not TAUTULLI_APIKEY:
|
||||
TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')
|
||||
if not TAUTULLI_PUBLIC_URL:
|
||||
TAUTULLI_PUBLIC_URL = CONFIG.data['auth'].get('tautulli_public_url')
|
||||
|
||||
VERIFY_SSL = False
|
||||
|
||||
if TAUTULLI_PUBLIC_URL != '/':
|
||||
# Check to see if there is a public URL set in Tautulli
|
||||
TAUTULLI_LINK = TAUTULLI_PUBLIC_URL
|
||||
else:
|
||||
TAUTULLI_LINK = TAUTULLI_URL
|
||||
|
||||
RICH_TYPE = ['discord', 'slack']
|
||||
|
||||
# Colors for rich notifications
|
||||
SECTIONS_COLOR = 10964298
|
||||
USERS_COLOR = 10964298
|
||||
|
||||
# Author name for rich notifications
|
||||
AUTHOR_NAME = 'My Server'
|
||||
|
||||
TAUTULLI_ICON = 'https://github.com/Tautulli/Tautulli/raw/master/data/interfaces/default/images/logo-circle.png'
|
||||
|
||||
SUBJECT_TEXT = "Tautulli Statistics"
|
||||
|
||||
# Notification notifier ID: https://github.com/JonnyWong16/plexpy/blob/master/API.md#notify
|
||||
NOTIFIER_ID = 10 # The email notification notifier ID for Tautulli
|
||||
NOTIFIER_ID = 12 # The email notification notifier ID for Tautulli
|
||||
|
||||
# Remove library element you do not want shown. Logging before exclusion.
|
||||
# SHOW_STAT = 'Shows: {0}, Episodes: {2}'
|
||||
@ -40,16 +76,21 @@ LIB_IGNORE = ['XXX']
|
||||
|
||||
# Customize user stats display
|
||||
# User: USER1 -> 1 hr 32 min 00 sec
|
||||
USER_STAT = 'User: {0} -> {1}'
|
||||
USER_STAT = '{0} -> {1}'
|
||||
|
||||
# Usernames you do not want shown. Logging before exclusion.
|
||||
USER_IGNORE = ['User1']
|
||||
|
||||
# User stat choices
|
||||
STAT_CHOICE = ['duration', 'plays']
|
||||
|
||||
# Customize time display
|
||||
# {0:d} hr {1:02d} min {2:02d} sec --> 1 hr 32 min 00 sec
|
||||
# {0:d} hr {1:02d} min --> 1 hr 32 min
|
||||
# {0:02d} hr {1:02d} min --> 01 hr 32 min
|
||||
TIME_DISPLAY = "{0:d} hr {1:02d} min {2:02d} sec"
|
||||
# {0:d} day(s) {1:d} hr {2:02d} min {3:02d} sec --> 1 day(s) 0 hr 34 min 02 sec
|
||||
# {1:d} hr {2:02d} min {3:02d} sec --> 1 hr 32 min 00 sec
|
||||
# {1:d} hr {2:02d} min --> 1 hr 32 min
|
||||
# {1:02d} hr {2:02d} min --> 01 hr 32 min
|
||||
# 0 = days, 1 = hours, 2 = minutes, 3 = seconds
|
||||
TIME_DISPLAY = "{0:d} day(s) {1:d} hr {2:02d} min {3:02d} sec"
|
||||
|
||||
# Customize BODY to your liking
|
||||
BODY_TEXT = """\
|
||||
@ -72,91 +113,26 @@ BODY_TEXT = """\
|
||||
|
||||
# /EDIT THESE SETTINGS #
|
||||
|
||||
def get_history(section_id, check_date):
|
||||
# Get the Tautulli history.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_history',
|
||||
'section_id': section_id,
|
||||
'start_date': check_date}
|
||||
|
||||
|
||||
def utc_now_iso():
|
||||
"""Get current time in ISO format"""
|
||||
utcnow = datetime.utcnow()
|
||||
|
||||
return utcnow.isoformat()
|
||||
|
||||
|
||||
def hex_to_int(value):
|
||||
"""Convert hex value to integer"""
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
# print(json.dumps(response['response']['data'], indent=4, sort_keys=True))
|
||||
res_data = response['response']['data']
|
||||
if res_data['filter_duration'] != '0':
|
||||
return res_data['data']
|
||||
else:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_libraries():
|
||||
# Get a list of all libraries on your server.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_libraries'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
# print(json.dumps(response['response']['data'], indent=4, sort_keys=True))
|
||||
res_data = response['response']['data']
|
||||
return res_data
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_libraries' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_library_media_info(section_id):
|
||||
# Get a list of all libraries on your server.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_library_media_info',
|
||||
'section_id': section_id}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
# print(json.dumps(response['response']['data'], indent=4, sort_keys=True))
|
||||
res_data = response['response']['data']
|
||||
return res_data['total_file_size']
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def send_notification(body_text):
|
||||
# Format notification text
|
||||
try:
|
||||
subject = SUBJECT_TEXT
|
||||
body = body_text
|
||||
except LookupError as e:
|
||||
sys.stderr.write("Unable to substitute '{0}' in the notification subject or body".format(e))
|
||||
return None
|
||||
# Send the notification through Tautulli
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'notify',
|
||||
'notifier_id': NOTIFIER_ID,
|
||||
'subject': subject,
|
||||
'body': body}
|
||||
|
||||
try:
|
||||
r = requests.post(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
|
||||
if response['response']['result'] == 'success':
|
||||
sys.stdout.write("Successfully sent Tautulli notification.")
|
||||
else:
|
||||
raise Exception(response['response']['message'])
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'notify' request failed: {0}.".format(e))
|
||||
return None
|
||||
return int(value, 16)
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
# Function found https://stackoverflow.com/a/1094933
|
||||
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
@ -171,7 +147,6 @@ def date_split(to_split):
|
||||
|
||||
|
||||
def add_to_dictval(d, key, val):
|
||||
#print(d, key, val)
|
||||
if key not in d:
|
||||
d[key] = val
|
||||
else:
|
||||
@ -180,115 +155,369 @@ def add_to_dictval(d, key, val):
|
||||
|
||||
def daterange(start_date, end_date):
|
||||
for n in range(int((end_date - start_date).days) + 1):
|
||||
yield start_date + datetime.timedelta(n)
|
||||
yield start_date + timedelta(n)
|
||||
|
||||
|
||||
def get_server_stats(date_ranges):
|
||||
section_count = ''
|
||||
total_size = 0
|
||||
sections_id_lst = []
|
||||
sections_stats_lst = []
|
||||
def get_user_stats(home_stats, rich, stats_type, notify=None):
|
||||
user_stats_lst = []
|
||||
user_stats_dict = {}
|
||||
user_names_lst = []
|
||||
user_durations_lst =[]
|
||||
print('Checking users stats.')
|
||||
for stats in home_stats:
|
||||
if stats['stat_id'] == 'top_users':
|
||||
for row in stats['rows']:
|
||||
if stats_type == 'duration':
|
||||
add_to_dictval(user_stats_dict, row['friendly_name'], row['total_duration'])
|
||||
else:
|
||||
add_to_dictval(user_stats_dict, row['friendly_name'], row['total_plays'])
|
||||
|
||||
for user, stat in sorted(user_stats_dict.items(), key=itemgetter(1), reverse=True):
|
||||
if user not in USER_IGNORE:
|
||||
if stats_type == 'duration':
|
||||
user_total = timedelta(seconds=stat)
|
||||
days = user_total.days
|
||||
hours, remainder = divmod(user_total.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
user_custom = TIME_DISPLAY.format(days, hours, minutes, seconds)
|
||||
USER_STATS = USER_STAT.format(user, user_custom)
|
||||
else:
|
||||
USER_STATS = USER_STAT.format(user, stat)
|
||||
if rich or not notify:
|
||||
user_stats_lst += ['{}'.format(USER_STATS)]
|
||||
else:
|
||||
# Html formatting
|
||||
user_stats_lst += ['<li>{}</li>'.format(USER_STATS)]
|
||||
|
||||
return user_stats_lst
|
||||
|
||||
|
||||
def get_library_stats(libraries, tautulli, rich, notify=None):
|
||||
section_count = ''
|
||||
total_size = 0
|
||||
sections_stats_lst = []
|
||||
|
||||
print('Checking library stats.')
|
||||
for sections in get_libraries():
|
||||
for section in libraries:
|
||||
|
||||
lib_size = get_library_media_info(sections['section_id'])
|
||||
total_size += lib_size
|
||||
sections_id_lst += [sections['section_id']]
|
||||
library = tautulli.get_library_media_info(section['section_id'])
|
||||
total_size += library['total_file_size']
|
||||
|
||||
if sections['section_type'] == 'artist':
|
||||
section_count = ARTIST_STAT.format(sections['count'], sections['parent_count'], sections['child_count'])
|
||||
if section['section_type'] == 'artist':
|
||||
section_count = ARTIST_STAT.format(section['count'], section['parent_count'], section['child_count'])
|
||||
|
||||
elif sections['section_type'] == 'show':
|
||||
section_count = SHOW_STAT.format(sections['count'], sections['parent_count'], sections['child_count'])
|
||||
elif section['section_type'] == 'show':
|
||||
section_count = SHOW_STAT.format(section['count'], section['parent_count'], section['child_count'])
|
||||
|
||||
elif sections['section_type'] == 'photo':
|
||||
section_count = PHOTO_STAT.format(sections['count'], sections['parent_count'], sections['child_count'])
|
||||
elif section['section_type'] == 'photo':
|
||||
section_count = PHOTO_STAT.format(section['count'], section['parent_count'], section['child_count'])
|
||||
|
||||
elif sections['section_type'] == 'movie':
|
||||
section_count = MOVIE_STAT.format(sections['count'])
|
||||
elif section['section_type'] == 'movie':
|
||||
section_count = MOVIE_STAT.format(section['count'])
|
||||
|
||||
if sections['section_name'] not in LIB_IGNORE and section_count:
|
||||
# Html formatting
|
||||
sections_stats_lst += ['<li>{}: {}</li>'.format(sections['section_name'], section_count)]
|
||||
if section['section_name'] not in LIB_IGNORE and section_count:
|
||||
if rich or not notify:
|
||||
sections_stats_lst += ['{}: {}'.format(section['section_name'], section_count)]
|
||||
else:
|
||||
# Html formatting
|
||||
sections_stats_lst += ['<li>{}: {}</li>'.format(section['section_name'], section_count)]
|
||||
|
||||
print('Checking users stats.')
|
||||
# print(sections_id_lst)
|
||||
for check_date in date_ranges:
|
||||
for section_id in sections_id_lst:
|
||||
# print(check_date, section_id)
|
||||
history = get_history(section_id, check_date)
|
||||
if history:
|
||||
# print(json.dumps(history, indent=4, sort_keys=True))
|
||||
for data in history:
|
||||
# print(data)
|
||||
user_names_lst += [data['friendly_name']]
|
||||
user_durations_lst += [data['duration']]
|
||||
# print(user_durations_lst, user_names_lst)
|
||||
for user_name, user_totals in zip(user_names_lst, user_durations_lst):
|
||||
add_to_dictval(user_stats_dict, user_name, user_totals)
|
||||
if rich or not notify:
|
||||
sections_stats_lst += ['Capacity: {}'.format(sizeof_fmt(total_size))]
|
||||
else:
|
||||
# Html formatting. Adding the Capacity to bottom of list.
|
||||
sections_stats_lst += ['<li>Capacity: {}</li>'.format(sizeof_fmt(total_size))]
|
||||
|
||||
print('{} watched something on {}'.format(' & '.join(set(user_names_lst)), check_date))
|
||||
# print(json.dumps(user_stats_dict, indent=4, sort_keys=True))
|
||||
for user, duration in sorted(user_stats_dict.items(), key=itemgetter(1), reverse=True):
|
||||
if user not in USER_IGNORE:
|
||||
m, s = divmod(duration, 60)
|
||||
h, m = divmod(m, 60)
|
||||
easy_time = TIME_DISPLAY.format(h, m, s)
|
||||
USER_STATS = USER_STAT.format(user, easy_time)
|
||||
# Html formatting
|
||||
user_stats_lst += ['<li>{}</li>'.format(USER_STATS)]
|
||||
|
||||
# Html formatting. Adding the Capacity to bottom of list.
|
||||
sections_stats_lst += ['<li>Capacity: {}</li>'.format(sizeof_fmt(total_size))]
|
||||
|
||||
# print(sections_stats_lst, user_stats_lst)
|
||||
return (sections_stats_lst, user_stats_lst)
|
||||
return sections_stats_lst
|
||||
|
||||
|
||||
def main():
|
||||
class Tautulli(object):
|
||||
def __init__(self, url, apikey, verify_ssl=False, debug=None):
|
||||
self.url = url
|
||||
self.apikey = apikey
|
||||
self.debug = debug
|
||||
|
||||
global BODY_TEXT
|
||||
self.session = Session()
|
||||
self.adapters = HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session.mount('http://', self.adapters)
|
||||
self.session.mount('https://', self.adapters)
|
||||
|
||||
# Ignore verifying the SSL certificate
|
||||
if verify_ssl is False:
|
||||
self.session.verify = False
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
def get_library_media_info(self, section_id=None, refresh=None):
|
||||
"""Call Tautulli's get_activity api endpoint"""
|
||||
payload = {}
|
||||
if refresh:
|
||||
for library in self.get_libraries():
|
||||
payload['section_id'] = library['section_id']
|
||||
payload['refresh'] = 'true'
|
||||
print('Refreshing library: {}'.format(library['section_name']))
|
||||
self._call_api('get_library_media_info', payload)
|
||||
print('Libraries have been refreshed, please wait while library stats are updated.')
|
||||
exit()
|
||||
else:
|
||||
payload['section_id'] = section_id
|
||||
|
||||
return self._call_api('get_library_media_info', payload)
|
||||
|
||||
def get_libraries(self):
|
||||
"""Call Tautulli's get_activity api endpoint"""
|
||||
payload = {}
|
||||
|
||||
return self._call_api('get_libraries', payload)
|
||||
|
||||
def get_home_stats(self, time_range, stats_type, stats_count):
|
||||
"""Call Tautulli's get_activity api endpoint"""
|
||||
payload = {}
|
||||
payload['time_range'] = time_range
|
||||
payload['stats_type'] = stats_type
|
||||
payload['stats_count'] = stats_count
|
||||
|
||||
return self._call_api('get_home_stats', payload)
|
||||
|
||||
def get_history(self, section_id, check_date):
|
||||
"""Call Tautulli's get_activity api endpoint"""
|
||||
payload = {}
|
||||
payload['section_id'] = int(section_id)
|
||||
payload['start_date'] = check_date
|
||||
|
||||
return self._call_api('get_history', payload)
|
||||
|
||||
def notify(self, notifier_id, subject, body):
|
||||
"""Call Tautulli's notify api endpoint"""
|
||||
payload = {'notifier_id': notifier_id,
|
||||
'subject': subject,
|
||||
'body': body}
|
||||
|
||||
return self._call_api('notify', payload)
|
||||
|
||||
def _call_api(self, cmd, payload, method='GET'):
|
||||
payload['cmd'] = cmd
|
||||
payload['apikey'] = self.apikey
|
||||
|
||||
try:
|
||||
response = self.session.request(method, self.url + '/api/v2', params=payload)
|
||||
except RequestException as e:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
|
||||
return
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
except ValueError:
|
||||
print(
|
||||
"Failed to parse json response for Tautulli API cmd '{}': {}"
|
||||
.format(cmd, response.content))
|
||||
return
|
||||
|
||||
if response_json['response']['result'] == 'success':
|
||||
if self.debug:
|
||||
print("Successfully called Tautulli API cmd '{}'".format(cmd))
|
||||
return response_json['response']['data']
|
||||
else:
|
||||
error_msg = response_json['response']['message']
|
||||
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
|
||||
return
|
||||
|
||||
|
||||
class Notification(object):
|
||||
def __init__(self, notifier_id, subject, body, tautulli, stats=None):
|
||||
self.notifier_id = notifier_id
|
||||
self.subject = subject
|
||||
self.body = body
|
||||
|
||||
self.tautulli = tautulli
|
||||
if stats:
|
||||
self.stats = stats
|
||||
|
||||
def send(self, subject='', body=''):
|
||||
"""Send to Tautulli notifier.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
subject : str
|
||||
Subject of the message.
|
||||
body : str
|
||||
Body of the message.
|
||||
"""
|
||||
subject = subject or self.subject
|
||||
body = body or self.body
|
||||
self.tautulli.notify(notifier_id=self.notifier_id, subject=subject, body=body)
|
||||
|
||||
def send_discord(self, title, color, stat, footer):
|
||||
"""Build the Discord message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title of the message.
|
||||
color : int
|
||||
The color of the message
|
||||
"""
|
||||
discord_message = {
|
||||
"embeds": [
|
||||
{
|
||||
"author": {
|
||||
"icon_url": TAUTULLI_ICON,
|
||||
"name": AUTHOR_NAME,
|
||||
},
|
||||
"color": color,
|
||||
"fields": [
|
||||
{
|
||||
"name": "{} Stats".format(stat),
|
||||
"value": "".join(self.stats)
|
||||
},
|
||||
],
|
||||
"title": title,
|
||||
"timestamp": utc_now_iso(),
|
||||
"footer": {
|
||||
"text": " to ".join(x for x in footer)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
discord_message = json.dumps(discord_message, sort_keys=True,
|
||||
separators=(',', ': '))
|
||||
self.send(body=discord_message)
|
||||
|
||||
def send_slack(self, title, color, stat):
|
||||
"""Build the Slack message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title of the message.
|
||||
color : int
|
||||
The color of the message
|
||||
poster_url : str
|
||||
The media poster URL.
|
||||
plex_url : str
|
||||
Plex media URL.
|
||||
message : str
|
||||
Message sent to the player.
|
||||
footer : str
|
||||
Footer of the message.
|
||||
"""
|
||||
slack_message = {
|
||||
"attachments": [
|
||||
{
|
||||
"title": title,
|
||||
"author_icon": TAUTULLI_ICON,
|
||||
"author_name": AUTHOR_NAME,
|
||||
"author_link": TAUTULLI_LINK.rstrip('/'),
|
||||
"color": color,
|
||||
"fields": [
|
||||
{
|
||||
"title": "{} Stats".format(stat),
|
||||
"value": self.stats,
|
||||
"short": True
|
||||
},
|
||||
],
|
||||
"ts": time.time()
|
||||
}
|
||||
|
||||
],
|
||||
}
|
||||
|
||||
slack_message = json.dumps(slack_message, sort_keys=True,
|
||||
separators=(',', ': '))
|
||||
self.send(body=slack_message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Use Tautulli to pull library and user statistics for date range.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('-d', '--days', default=7, metavar='', type=int,
|
||||
help='Enter in number of days to go back. \n(default: %(default)s)')
|
||||
parser.add_argument('-t', '--top', default=5, metavar='', type=int,
|
||||
help='Enter in number of top users to find. \n(default: %(default)s)')
|
||||
parser.add_argument('--stat', default='duration', choices=STAT_CHOICE,
|
||||
help='Enter in number of top users to find. \n(default: %(default)s)')
|
||||
parser.add_argument('--notify', type=int,
|
||||
help='Notification Agent ID number to Agent to '
|
||||
'send notification.')
|
||||
parser.add_argument('--richMessage', choices=RICH_TYPE,
|
||||
help='Rich message type selector.\nChoices: (%(choices)s)')
|
||||
parser.add_argument('--refresh', action='store_true',
|
||||
help='Refresh all libraries in Tautulli')
|
||||
parser.add_argument('--libraryStats', action='store_true',
|
||||
help='Only retrieve library stats.')
|
||||
parser.add_argument('--userStats', action='store_true',
|
||||
help='Only retrieve users stats.')
|
||||
|
||||
# todo Goals: growth reporting? show library size growth over time?
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
tautulli_server = Tautulli(TAUTULLI_URL.rstrip('/'), TAUTULLI_APIKEY, VERIFY_SSL)
|
||||
|
||||
if opts.refresh:
|
||||
tautulli_server.get_library_media_info(refresh=True)
|
||||
|
||||
TODAY = int(time.time())
|
||||
DAYS = opts.days
|
||||
DAYS_AGO = int(TODAY - DAYS * 24 * 60 * 60)
|
||||
START_DATE = (datetime.datetime.utcfromtimestamp(DAYS_AGO).strftime("%Y-%m-%d")) # DAYS_AGO as YYYY-MM-DD
|
||||
END_DATE = (datetime.datetime.utcfromtimestamp(TODAY).strftime("%Y-%m-%d")) # TODAY as YYYY-MM-DD
|
||||
START_DATE = (datetime.utcfromtimestamp(DAYS_AGO).strftime("%Y-%m-%d")) # DAYS_AGO as YYYY-MM-DD
|
||||
END_DATE = (datetime.utcfromtimestamp(TODAY).strftime("%Y-%m-%d")) # TODAY as YYYY-MM-DD
|
||||
|
||||
start_date = datetime.date(date_split(START_DATE)[0], date_split(START_DATE)[1], date_split(START_DATE)[2])
|
||||
end_date = datetime.date(date_split(END_DATE)[0], date_split(END_DATE)[1], date_split(END_DATE)[2])
|
||||
start_date = date(date_split(START_DATE)[0], date_split(START_DATE)[1], date_split(START_DATE)[2])
|
||||
end_date = date(date_split(END_DATE)[0], date_split(END_DATE)[1], date_split(END_DATE)[2])
|
||||
|
||||
dates_range_lst = []
|
||||
for single_date in daterange(start_date, end_date):
|
||||
dates_range_lst += [single_date.strftime("%Y-%m-%d")]
|
||||
|
||||
end = datetime.strptime(time.ctime(float(TODAY)), "%a %b %d %H:%M:%S %Y").strftime("%a %b %d %Y")
|
||||
start = datetime.strptime(time.ctime(float(DAYS_AGO)), "%a %b %d %H:%M:%S %Y").strftime("%a %b %d %Y")
|
||||
|
||||
print('Checking user stats from {:02d} days ago.'.format(opts.days))
|
||||
sections_stats = ''
|
||||
if opts.libraryStats or (not opts.libraryStats and not opts.userStats):
|
||||
libraries = tautulli_server.get_libraries()
|
||||
lib_stats = get_library_stats(libraries, tautulli_server, opts.richMessage, opts.notify)
|
||||
sections_stats = "\n".join(lib_stats)
|
||||
|
||||
lib_stats, user_stats_lst = get_server_stats(dates_range_lst)
|
||||
# print(lib_stats)
|
||||
user_stats = ''
|
||||
if opts.userStats or (not opts.libraryStats and not opts.userStats):
|
||||
print('Checking user stats from {:02d} days ago.'.format(opts.days))
|
||||
home_stats = tautulli_server.get_home_stats(opts.days, opts.stat, opts.top)
|
||||
user_stats_lst = get_user_stats(home_stats, opts.richMessage, opts.stat, opts.notify)
|
||||
user_stats = "\n".join(user_stats_lst)
|
||||
|
||||
end = datetime.datetime.strptime(time.ctime(float(TODAY)), "%a %b %d %H:%M:%S %Y").strftime("%a %b %d %Y")
|
||||
start = datetime.datetime.strptime(time.ctime(float(DAYS_AGO)), "%a %b %d %H:%M:%S %Y").strftime("%a %b %d %Y")
|
||||
|
||||
sections_stats = "\n".join(lib_stats)
|
||||
user_stats = "\n".join(user_stats_lst)
|
||||
|
||||
BODY_TEXT = BODY_TEXT.format(end=end, start=start, sections_stats=sections_stats, user_stats=user_stats)
|
||||
|
||||
print('Sending message.')
|
||||
send_notification(BODY_TEXT)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
if opts.notify and opts.richMessage:
|
||||
user_notification = ''
|
||||
if user_stats:
|
||||
user_notification = Notification(opts.notify, None, None, tautulli_server, user_stats)
|
||||
section_notification = ''
|
||||
if sections_stats:
|
||||
section_notification= Notification(opts.notify, None, None, tautulli_server, sections_stats)
|
||||
if opts.richMessage == 'slack':
|
||||
if user_notification:
|
||||
user_notification.send_slack(SUBJECT_TEXT, USERS_COLOR, 'User ' + opts.stat.capitalize())
|
||||
if section_notification:
|
||||
section_notification.send_slack(SUBJECT_TEXT, SECTIONS_COLOR, 'Section')
|
||||
elif opts.richMessage == 'discord':
|
||||
if user_notification:
|
||||
user_notification.send_discord(SUBJECT_TEXT, USERS_COLOR, 'User ' + opts.stat.capitalize(),
|
||||
footer=(end,start))
|
||||
if section_notification:
|
||||
section_notification.send_discord(SUBJECT_TEXT, SECTIONS_COLOR, 'Section', footer=(end,start))
|
||||
elif opts.notify and not opts.richMessage:
|
||||
BODY_TEXT = BODY_TEXT.format(end=end, start=start, sections_stats=sections_stats, user_stats=user_stats)
|
||||
print('Sending message.')
|
||||
notify = Notification(opts.notify, SUBJECT_TEXT, BODY_TEXT, tautulli_server)
|
||||
notify.send()
|
||||
else:
|
||||
if sections_stats:
|
||||
print('Section Stats:\n{}'.format(''.join(sections_stats)))
|
||||
if user_stats:
|
||||
print('User {} Stats:\n{}'.format(opts.stat.capitalize(), ''.join(user_stats)))
|
@ -4,6 +4,4 @@
|
||||
#---------------------------------------------------------
|
||||
requests
|
||||
plexapi
|
||||
matplotlib
|
||||
numpy
|
||||
basemap
|
||||
urllib3
|
@ -4,10 +4,10 @@ Author: {author}
|
||||
Requires: {requirements}
|
||||
|
||||
Enabling Scripts in Tautulli:
|
||||
Taultulli > Settings > Notification Agents > Add a Notification Agent > Script
|
||||
Tautulli > Settings > Notification Agents > Add a Notification Agent > Script
|
||||
|
||||
Configuration:
|
||||
Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Tautulli > Settings > Notification Agents > New Script > Configuration:
|
||||
|
||||
Script Name: {script_name}
|
||||
Set Script Timeout: {timeout}
|
||||
@ -15,19 +15,19 @@ Taultulli > Settings > Notification Agents > New Script > Configuration:
|
||||
Save
|
||||
|
||||
Triggers:
|
||||
Taultulli > Settings > Notification Agents > New Script > Triggers:
|
||||
Tautulli > Settings > Notification Agents > New Script > Triggers:
|
||||
|
||||
Check: {trigger}
|
||||
Save
|
||||
|
||||
Conditions:
|
||||
Taultulli > Settings > Notification Agents > New Script > Conditions:
|
||||
Tautulli > Settings > Notification Agents > New Script > Conditions:
|
||||
|
||||
Set Conditions: [{condition} | {operator} | {value} ]
|
||||
Save
|
||||
|
||||
Script Arguments:
|
||||
Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
Tautulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
Select: {trigger}
|
||||
Arguments: {arguments}
|
||||
|
11
setup.cfg
Normal file
11
setup.cfg
Normal file
@ -0,0 +1,11 @@
|
||||
; Contains configuration for various linters
|
||||
|
||||
; E501: Disable line length limits (for now)
|
||||
; W504: Require newlines after binary operators, use W503 for requiring the
|
||||
; operators on the next line
|
||||
|
||||
[flake8]
|
||||
ignore = E501,W504
|
||||
|
||||
[pylama]
|
||||
ignore = E501,W504
|
64
utility/add_label_recently_added.py
Normal file
64
utility/add_label_recently_added.py
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Description: Automatically add a label to recently added items in your Plex library
|
||||
Author: /u/SwiftPanda16
|
||||
Requires: plexapi
|
||||
Usage:
|
||||
python add_label_recently_added.py --rating_key 1234 --label "Label"
|
||||
|
||||
Tautulli script trigger:
|
||||
* Notify on recently added
|
||||
Tautulli script conditions:
|
||||
* Filter which media to add labels to using conditions. Examples:
|
||||
[ Media Type | is | movie ]
|
||||
[ Show Name | is | Game of Thrones ]
|
||||
[ Album Name | is | Reputation ]
|
||||
[ Video Resolution | is | 4k ]
|
||||
[ Genre | contains | horror ]
|
||||
Tautulli script arguments:
|
||||
* Recently Added:
|
||||
--rating_key {rating_key} --label "Label"
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
|
||||
# ## OVERRIDES - ONLY EDIT IF RUNNING SCRIPT WITHOUT TAUTULLI ##
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Environmental Variables
|
||||
PLEX_URL = PLEX_URL or os.getenv('PLEX_URL', PLEX_URL)
|
||||
PLEX_TOKEN = PLEX_TOKEN or os.getenv('PLEX_TOKEN', PLEX_TOKEN)
|
||||
|
||||
|
||||
def add_label_parent(plex, rating_key, label):
|
||||
item = plex.fetchItem(rating_key)
|
||||
|
||||
if item.type in ('movie', 'show', 'album'):
|
||||
parent = item
|
||||
elif item.type in ('season', 'episode'):
|
||||
parent = item.show()
|
||||
elif item.type == 'track':
|
||||
parent = item.album()
|
||||
else:
|
||||
print(f"Cannot add label to '{item.title}' ({item.ratingKey}): Invalid media type '{item.type}'")
|
||||
return
|
||||
|
||||
print(f"Adding label '{label}' to '{parent.title}' ({parent.ratingKey})")
|
||||
parent.addLabel(label)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rating_key', required=True, type=int)
|
||||
parser.add_argument('--label', required=True)
|
||||
opts = parser.parse_args()
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
add_label_parent(plex, **vars(opts))
|
@ -1,4 +1,7 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Use Tautulli to pull last IP address from user and add to List of IP addresses and networks that are allowed without auth in Plex.
|
||||
|
||||
optional arguments:
|
||||
@ -12,14 +15,16 @@ optional arguments:
|
||||
(default: None)
|
||||
|
||||
List of IP addresses is cleared before adding new IPs
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
PLEX_TOKEN = 'xxxx'
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
TAUTULLI_APIKEY = 'xxxx' # Your Tautulli API key
|
||||
@ -77,7 +82,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('-u', '--users', nargs='+', type=str, choices=user_lst, metavar='',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s) \n(default: %(default)s)')
|
||||
parser.add_argument('-c', '--clear', nargs='?',default=None, metavar='',
|
||||
parser.add_argument('-c', '--clear', nargs='?', default=None, metavar='',
|
||||
help='Clear List of IP addresses and networks that are allowed without auth in Plex: \n'
|
||||
'(default: %(default)s)')
|
||||
|
||||
|
@ -1,35 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
From a list of TV shows, check if users in a list has watched shows episodes.
|
||||
If all users in list have watched an episode of listed show, then delete episode.
|
||||
|
||||
Add deletion via Plex.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'xxxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
SHOW_LST = [123456, 123456, 123456, 123456] # Show rating keys.
|
||||
USER_LST = ['Sam', 'Jakie', 'Blacktwin'] # Name of users
|
||||
|
||||
|
||||
class UserHIS(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.rating_key = d['rating_key']
|
||||
|
||||
|
||||
class METAINFO(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.title = d['title']
|
||||
media_info = d['media_info'][0]
|
||||
parts = media_info['parts'][0]
|
||||
self.file_size = parts['file_size']
|
||||
self.file = parts['file']
|
||||
self.media_type = d['media_type']
|
||||
self.grandparent_title = d['grandparent_title']
|
||||
@ -68,7 +66,7 @@ def get_history(user, show, start, length):
|
||||
response = r.json()
|
||||
|
||||
res_data = response['response']['data']['data']
|
||||
return [UserHIS(data=d) for d in res_data if d['watched_status'] == 1]
|
||||
return [d['rating_key'] for d in res_data if d['watched_status'] == 1]
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
@ -88,9 +86,9 @@ for user in USER_LST:
|
||||
try:
|
||||
if all([history]):
|
||||
start += count
|
||||
for h in history:
|
||||
for rating_key in history:
|
||||
# Getting metadata of what was watched
|
||||
meta = get_metadata(h.rating_key)
|
||||
meta = get_metadata(rating_key)
|
||||
if not any(d['title'] == meta.title for d in meta_lst):
|
||||
meta_dict = {
|
||||
'title': meta.title,
|
||||
@ -115,12 +113,10 @@ for user in USER_LST:
|
||||
|
||||
|
||||
for meta_dict in meta_lst:
|
||||
for key, value in meta_dict.items():
|
||||
if value == USER_LST:
|
||||
print(u"{} {} has been watched by {}".format(meta_dict['grandparent_title'], meta_dict['title'],
|
||||
" & ".join(USER_LST)))
|
||||
delete_lst.append(meta_dict['file'])
|
||||
|
||||
for x in delete_lst:
|
||||
print("Removing {}".format(x))
|
||||
os.remove(x)
|
||||
if set(USER_LST) == set(meta_dict['watched_by']):
|
||||
print("{} {} has been watched by {}".format(
|
||||
meta_dict['grandparent_title'].encode('UTF-8'),
|
||||
meta_dict['title'].encode('UTF-8'),
|
||||
" & ".join(USER_LST)))
|
||||
print("Removing {}".format(meta_dict['file']))
|
||||
os.remove(meta_dict['file'])
|
||||
|
52
utility/enable_disable_all_guest_access.py
Normal file
52
utility/enable_disable_all_guest_access.py
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Enable or disable all users remote access to Tautulli.
|
||||
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, python3.6+
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from requests import Session
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
ENABLE_REMOTE_ACCESS = True
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_API_KEY = ''
|
||||
|
||||
# Do not edit past this line #
|
||||
session = Session()
|
||||
session.params = {'apikey': TAUTULLI_API_KEY}
|
||||
formatted_url = f'{TAUTULLI_URL}/api/v2'
|
||||
|
||||
request = session.get(formatted_url, params={'cmd': 'get_users'})
|
||||
|
||||
tautulli_users = None
|
||||
try:
|
||||
tautulli_users = request.json()['response']['data']
|
||||
except JSONDecodeError:
|
||||
exit("Error talking to Tautulli API, please check your TAUTULLI_URL")
|
||||
|
||||
allow_guest = 1 if ENABLE_REMOTE_ACCESS else 0
|
||||
string_representation = 'Enabled' if ENABLE_REMOTE_ACCESS else 'Disabled'
|
||||
|
||||
users_to_change = [user for user in tautulli_users if user['allow_guest'] != allow_guest]
|
||||
|
||||
if users_to_change:
|
||||
for user in users_to_change:
|
||||
# Redefine ALL params because of Tautulli edit_user API bug
|
||||
params = {
|
||||
'cmd': 'edit_user',
|
||||
'user_id': user['user_id'],
|
||||
'friendly_name': user['friendly_name'],
|
||||
'custom_thumb': user['custom_thumb'],
|
||||
'keep_history': user['keep_history'],
|
||||
'allow_guest': allow_guest
|
||||
}
|
||||
changed_user = session.get(formatted_url, params=params)
|
||||
print(f"{string_representation} guest access for {user['friendly_name']}")
|
||||
else:
|
||||
print(f'No users to {string_representation.lower()[:-1]}')
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Find location of Plex metadata.
|
||||
|
||||
@ -7,26 +10,44 @@ or
|
||||
find_plex_meta.py -s adventure -m movie
|
||||
pulls all movie titles with adventure in the title
|
||||
'''
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
# pip install plexapi
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import argparse
|
||||
import requests
|
||||
|
||||
## Edit ##
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxx'
|
||||
# ## Edit ##
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
# Change directory based on your os see:
|
||||
# https://support.plex.tv/hc/en-us/articles/202915258-Where-is-the-Plex-Media-Server-data-directory-located-
|
||||
PLEX_LOCAL_TV_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\TV Shows')
|
||||
PLEX_LOCAL_MOVIE_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\Movies')
|
||||
PLEX_LOCAL_ALBUM_PATH = os.path.join(os.getenv('LOCALAPPDATA'), 'Plex Media Server\Metadata\Albums')
|
||||
## /Edit ##
|
||||
# ## /Edit ##
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
def hash_to_path(hash_str, path, title, media_type, artist=None):
|
||||
full_hash = hashlib.sha1(hash_str).hexdigest()
|
||||
@ -38,12 +59,11 @@ def hash_to_path(hash_str, path, title, media_type, artist=None):
|
||||
output = "{} titled: {}\nPath: {}".format(media_type.title(), title, full_path)
|
||||
print(output)
|
||||
|
||||
|
||||
def get_plex_hash(search, mediatype=None):
|
||||
for searched in plex.search(search, mediatype=mediatype):
|
||||
# Remove special characters from name
|
||||
clean_title = re.sub('\W+',' ', searched.title)
|
||||
if searched.type == 'show':
|
||||
# Need to find guid.
|
||||
if searched.type == 'show':
|
||||
# Get tvdb_if from first episode
|
||||
db_id = searched.episodes()[0].guid
|
||||
# Find str to pop
|
||||
@ -63,7 +83,7 @@ def get_plex_hash(search, mediatype=None):
|
||||
hash_str = 'local://{}'.format(local_id)
|
||||
else:
|
||||
hash_str = searched.tracks()[0].guid.replace('/1?lang=en', '?lang=en')
|
||||
#print(searched.__dict__.items())
|
||||
# print(searched.__dict__.items())
|
||||
hash_to_path(hash_str, PLEX_LOCAL_ALBUM_PATH, searched.title, searched.type, searched.parentTitle)
|
||||
|
||||
elif searched.type == 'artist':
|
||||
@ -73,6 +93,7 @@ def get_plex_hash(search, mediatype=None):
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Helping navigate Plex's locally stored data.")
|
||||
parser.add_argument('-s', '--search', required=True, help='Search Plex for title.')
|
||||
|
@ -1,21 +1,28 @@
|
||||
"""
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Find what was added TFRAME ago and not watched using Tautulli.
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import input
|
||||
from builtins import str
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
|
||||
TFRAME = 1.577e+7 # ~ 6 months in seconds
|
||||
TFRAME = 1.577e+7 # ~ 6 months in seconds
|
||||
TODAY = time.time()
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
LIBRARY_NAMES = ['My TV Shows', 'My Movies'] # Name of libraries you want to check.
|
||||
LIBRARY_NAMES = ['My TV Shows', 'My Movies'] # Name of libraries you want to check.
|
||||
|
||||
|
||||
class LIBINFO(object):
|
||||
@ -80,7 +87,7 @@ def get_metadata(rating_key):
|
||||
res_data = response['response']['data']
|
||||
return METAINFO(data=res_data)
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# sys.stderr.write("Tautulli API 'get_metadata' request failed: {0}.".format(e))
|
||||
pass
|
||||
|
||||
@ -102,6 +109,7 @@ def get_library_media_info(section_id):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_library_media_info' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def get_libraries_table():
|
||||
# Get the data on the Tautulli libraries table.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -116,9 +124,10 @@ def get_libraries_table():
|
||||
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_libraries_table' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
|
||||
def delete_files(tmp_lst):
|
||||
del_file = raw_input('Delete all unwatched files? (yes/no)').lower()
|
||||
del_file = input('Delete all unwatched files? (yes/no)').lower()
|
||||
if del_file.startswith('y'):
|
||||
for x in tmp_lst:
|
||||
print("Removing {}".format(x))
|
||||
@ -126,6 +135,7 @@ def delete_files(tmp_lst):
|
||||
else:
|
||||
print('Ok. doing nothing.')
|
||||
|
||||
|
||||
show_lst = []
|
||||
path_lst = []
|
||||
|
||||
@ -143,10 +153,10 @@ for i in glt:
|
||||
# Find movie rating_key.
|
||||
show_lst += [int(x.rating_key)]
|
||||
except Exception as e:
|
||||
print("Rating_key failed: {e}").format(e=e)
|
||||
print(("Rating_key failed: {e}").format(e=e))
|
||||
|
||||
except Exception as e:
|
||||
print("Library media info failed: {e}").format(e=e)
|
||||
print(("Library media info failed: {e}").format(e=e))
|
||||
|
||||
# Remove reverse sort if you want the oldest keys first.
|
||||
for i in sorted(show_lst, reverse=True):
|
||||
@ -155,16 +165,16 @@ for i in sorted(show_lst, reverse=True):
|
||||
added = time.ctime(float(x.added_at))
|
||||
if x.grandparent_title == '' or x.media_type == 'movie':
|
||||
# Movies
|
||||
print(u"{x.title} ({x.rating_key}) was added {when} and has not been"
|
||||
print(u"{x.title} ({x.rating_key}) was added {when} and has not been "
|
||||
u"watched. \n File location: {x.file}".format(x=x, when=added))
|
||||
else:
|
||||
# Shows
|
||||
print(u"{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when} and has"
|
||||
print(u"{x.grandparent_title}: {x.title} ({x.rating_key}) was added {when} and has "
|
||||
u"not been watched. \n File location: {x.file}".format(x=x, when=added))
|
||||
path_lst += [x.file]
|
||||
|
||||
except Exception as e:
|
||||
print("Metadata failed. Likely end of range: {e}").format(e=e)
|
||||
print(("Metadata failed. Likely end of range: {e}").format(e=e))
|
||||
|
||||
|
||||
delete_files(path_lst)
|
||||
|
67
utility/get_serial_transcoders.py
Normal file
67
utility/get_serial_transcoders.py
Normal file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Get a list of "Serial Transcoders".
|
||||
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, plexapi, python3.6+
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
from past.utils import old_div
|
||||
from requests import Session
|
||||
from plexapi.server import CONFIG
|
||||
from datetime import date, timedelta
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_API_KEY = ''
|
||||
|
||||
PAST_DAYS = 7
|
||||
THRESHOLD_PERCENT = 50
|
||||
|
||||
# Do not edit past this line #
|
||||
|
||||
TAUTULLI_URL = TAUTULLI_URL or CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
TAUTULLI_API_KEY = TAUTULLI_API_KEY or CONFIG.data['auth'].get('tautulli_apikey')
|
||||
|
||||
TODAY = date.today()
|
||||
START_DATE = TODAY - timedelta(days=PAST_DAYS)
|
||||
|
||||
SESSION = Session()
|
||||
SESSION.params = {'apikey': TAUTULLI_API_KEY}
|
||||
FORMATTED_URL = f'{TAUTULLI_URL}/api/v2'
|
||||
|
||||
PARAMS = {'cmd': 'get_history', 'grouping': 1, 'order_column': 'date', 'length': 1000}
|
||||
|
||||
REQUEST = None
|
||||
try:
|
||||
REQUEST = SESSION.get(FORMATTED_URL, params=PARAMS).json()['response']['data']['data']
|
||||
except JSONDecodeError:
|
||||
exit("Error talking to Tautulli API, please check your TAUTULLI_URL")
|
||||
|
||||
HISTORY = [play for play in REQUEST if date.fromtimestamp(play['started']) >= START_DATE]
|
||||
|
||||
USERS = {}
|
||||
for play in HISTORY:
|
||||
if not USERS.get(play['user_id']):
|
||||
USERS.update(
|
||||
{
|
||||
play['user_id']: {
|
||||
'direct play': 0,
|
||||
'copy': 0,
|
||||
'transcode': 0
|
||||
}
|
||||
}
|
||||
)
|
||||
USERS[play['user_id']][play['transcode_decision']] += 1
|
||||
|
||||
PARAMS = {'cmd': 'get_user', 'user_id': 0}
|
||||
for user, counts in USERS.items():
|
||||
TOTAL_PLAYS = counts['transcode'] + counts['direct play'] + counts['copy']
|
||||
TRANSCODE_PERCENT = round(old_div(counts['transcode'] * 100, TOTAL_PLAYS), 2)
|
||||
if TRANSCODE_PERCENT >= THRESHOLD_PERCENT:
|
||||
PARAMS['user_id'] = user
|
||||
NAUGHTY = SESSION.get(FORMATTED_URL, params=PARAMS).json()['response']['data']
|
||||
print(f"{NAUGHTY['friendly_name']} is a serial transocde offender above the threshold at {TRANSCODE_PERCENT}%")
|
156
utility/gmusic_playlists_to_plex.py
Normal file
156
utility/gmusic_playlists_to_plex.py
Normal file
@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Pull Playlists from Google Music and create Playlist in Plex
|
||||
Author: Blacktwin, pjft, sdlynx
|
||||
Requires: gmusicapi, plexapi, requests
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
from gmusicapi import Mobileclient
|
||||
|
||||
import requests
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
MUSIC_LIBRARY_NAME = 'Music'
|
||||
|
||||
## CODE BELOW ##
|
||||
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl')
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
|
||||
|
||||
# Connect to Plex Server
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
# Connect to Google Music, if not authorized prompt to authorize
|
||||
# See https://unofficial-google-music-api.readthedocs.io/en/latest/reference/mobileclient.html
|
||||
# for more information
|
||||
mc = Mobileclient()
|
||||
if not mc.oauth_login(device_id=Mobileclient.FROM_MAC_ADDRESS):
|
||||
mc.perform_oauth()
|
||||
GGMUSICLIST = mc.get_all_songs()
|
||||
PLEX_MUSIC_LIBRARY = plex.library.section(MUSIC_LIBRARY_NAME)
|
||||
|
||||
def round_down(num, divisor):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
num (int,str): Number to round down
|
||||
divisor (int): Rounding digit
|
||||
|
||||
Returns
|
||||
-------
|
||||
Rounded down int
|
||||
"""
|
||||
num = int(num)
|
||||
return num - (num%divisor)
|
||||
|
||||
|
||||
def compare(ggmusic, pmusic):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
ggmusic (dict): Contains track data from Google Music
|
||||
pmusic (object): Plex item found from search
|
||||
|
||||
Returns
|
||||
-------
|
||||
pmusic (object): Matched Plex item
|
||||
"""
|
||||
title = str(ggmusic['title'].encode('ascii', 'ignore'))
|
||||
album = str(ggmusic['album'].encode('ascii', 'ignore'))
|
||||
tracknum = int(ggmusic['trackNumber'])
|
||||
duration = int(ggmusic['durationMillis'])
|
||||
|
||||
# Check if track numbers match
|
||||
if int(pmusic.index) == int(tracknum):
|
||||
return [pmusic]
|
||||
# If not track number, check track title and album title
|
||||
elif title == pmusic.title and (album == pmusic.parentTitle or
|
||||
album.startswith(pmusic.parentTitle)):
|
||||
return [pmusic]
|
||||
# Check if track duration match
|
||||
elif round_down(duration, 1000) == round_down(pmusic.duration, 1000):
|
||||
return [pmusic]
|
||||
# Lastly, check if title matches
|
||||
elif title == pmusic.title:
|
||||
return [pmusic]
|
||||
|
||||
def get_ggmusic(trackId):
|
||||
for ggmusic in GGMUSICLIST:
|
||||
if ggmusic['id'] == trackId:
|
||||
return ggmusic
|
||||
|
||||
def main():
|
||||
for pl in mc.get_all_user_playlist_contents():
|
||||
playlistName = pl['name']
|
||||
# Check for existing Plex Playlists, skip if exists
|
||||
if playlistName in [x.title for x in plex.playlists()]:
|
||||
print("Playlist: ({}) already available, skipping...".format(playlistName))
|
||||
else:
|
||||
playlistContent = []
|
||||
shareToken = pl['shareToken']
|
||||
# Go through tracks in Google Music Playlist
|
||||
for ggmusicTrackInfo in pl['tracks']:
|
||||
ggmusic = get_ggmusic(ggmusicTrackInfo['trackId'])
|
||||
title = str(ggmusic['title'])
|
||||
album = str(ggmusic['album'])
|
||||
artist = str(ggmusic['artist'])
|
||||
# Search Plex for Album title and Track title
|
||||
albumTrackSearch = PLEX_MUSIC_LIBRARY.searchTracks(
|
||||
**{'album.title': album, 'track.title': title})
|
||||
# Check results
|
||||
if len(albumTrackSearch) == 1:
|
||||
playlistContent += albumTrackSearch
|
||||
if len(albumTrackSearch) > 1:
|
||||
for pmusic in albumTrackSearch:
|
||||
albumTrackFound = compare(ggmusic, pmusic)
|
||||
if albumTrackFound:
|
||||
playlistContent += albumTrackFound
|
||||
break
|
||||
# Nothing found from Album title and Track title
|
||||
if not albumTrackSearch or len(albumTrackSearch) == 0:
|
||||
# Search Plex for Track title
|
||||
trackSearch = PLEX_MUSIC_LIBRARY.searchTracks(
|
||||
**{'track.title': title})
|
||||
if len(trackSearch) == 1:
|
||||
playlistContent += trackSearch
|
||||
if len(trackSearch) > 1:
|
||||
for pmusic in trackSearch:
|
||||
trackFound = compare(ggmusic, pmusic)
|
||||
if trackFound:
|
||||
playlistContent += trackFound
|
||||
break
|
||||
# Nothing found from Track title
|
||||
if not trackSearch or len(trackSearch) == 0:
|
||||
# Search Plex for Artist
|
||||
artistSearch = PLEX_MUSIC_LIBRARY.searchTracks(
|
||||
**{'artist.title': artist})
|
||||
for pmusic in artistSearch:
|
||||
artistFound = compare(ggmusic, pmusic)
|
||||
if artistFound:
|
||||
playlistContent += artistFound
|
||||
break
|
||||
if not artistSearch or len(artistSearch) == 0:
|
||||
print(u"Could not find in Plex:\n\t{} - {} {}".format(artist, album, title))
|
||||
if len(playlistContent) != 0:
|
||||
print("Adding Playlist: {}".format(playlistName))
|
||||
print("Google Music Playlist: {}, has {} tracks. {} tracks were added to Plex.".format(
|
||||
playlistName, len(pl['tracks']), len(playlistContent)))
|
||||
plex.createPlaylist(playlistName, playlistContent)
|
||||
else:
|
||||
print("Could not find any matching tracks in Plex for {}".format(playlistName))
|
||||
|
||||
main()
|
@ -1,9 +1,13 @@
|
||||
# -*- encoding: UTF-8 -*-
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
"""
|
||||
https://gist.github.com/blacktwin/f435aa0ccd498b0840d2407d599bf31d
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import input
|
||||
import os
|
||||
import httplib2
|
||||
|
||||
@ -12,15 +16,11 @@ from oauth2client.file import Storage
|
||||
from googleapiclient.discovery import build
|
||||
from oauth2client.client import OAuth2WebServerFlow
|
||||
|
||||
import time, shutil, sys
|
||||
|
||||
# Copy your credentials from the console
|
||||
# https://console.developers.google.com
|
||||
CLIENT_ID = ''
|
||||
CLIENT_SECRET = ''
|
||||
OUT_PATH = '' # Output Path
|
||||
|
||||
|
||||
OUT_PATH = '' # Output Path
|
||||
|
||||
OAUTH_SCOPE = 'https://www.googleapis.com/auth/drive'
|
||||
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
@ -37,7 +37,7 @@ if credentials is None:
|
||||
flow = OAuth2WebServerFlow(CLIENT_ID, CLIENT_SECRET, OAUTH_SCOPE, REDIRECT_URI)
|
||||
authorize_url = flow.step1_get_authorize_url()
|
||||
print('Go to the following link in your browser: ' + authorize_url)
|
||||
code = raw_input('Enter verification code: ').strip()
|
||||
code = input('Enter verification code: ').strip()
|
||||
credentials = flow.step2_exchange(code)
|
||||
storage.put(credentials)
|
||||
|
||||
@ -48,6 +48,7 @@ http = credentials.authorize(http)
|
||||
|
||||
drive_service = build('drive', 'v2', http=http)
|
||||
|
||||
|
||||
def list_files(service):
|
||||
page_token = None
|
||||
while True:
|
||||
@ -90,11 +91,11 @@ for item in list_files(drive_service):
|
||||
if 'mimeType' in item and 'image/jpeg' in item['mimeType'] or 'video/mp4' in item['mimeType']:
|
||||
download_url = item['downloadUrl']
|
||||
else:
|
||||
print 'ERROR getting %s' % item.get('title')
|
||||
print item
|
||||
print dir(item)
|
||||
print('ERROR getting %s' % item.get('title'))
|
||||
print(item)
|
||||
print(dir(item))
|
||||
if download_url:
|
||||
print "downloading %s" % item.get('title')
|
||||
print("downloading %s" % item.get('title'))
|
||||
resp, content = drive_service._http.request(download_url)
|
||||
if resp.status == 200:
|
||||
if os.path.isfile(outfile):
|
||||
|
134
utility/hide_episode_spoilers.py
Normal file
134
utility/hide_episode_spoilers.py
Normal file
@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Automatically change episode artwork in Plex to hide spoilers.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi, requests
|
||||
# Tautulli script trigger:
|
||||
# * Notify on recently added
|
||||
# * Notify on watched (optional - to remove the artwork after being watched)
|
||||
# Tautulli script conditions:
|
||||
# * Condition {1}:
|
||||
# [Media Type | is | show or season or episode]
|
||||
# * Condition {2} (optional):
|
||||
# [ Library Name | is | DVR ]
|
||||
# [ Show Namme | is | Game of Thrones ]
|
||||
# Tautulli script arguments:
|
||||
# * Recently Added:
|
||||
# To use an image file (can be image in the same directory as this script, or full path to an image):
|
||||
# --rating_key {rating_key} --image spoilers.png
|
||||
# To blur the episode artwork (optional blur in pixels):
|
||||
# --rating_key {rating_key} --blur 25
|
||||
# To add a prefix to the summary (optional string prefix):
|
||||
# --rating_key {rating_key} --summary_prefix "** SPOILERS **"
|
||||
# To upload the episode artwork instead of creating a local asset (optional, for when the script cannot access the media folder):
|
||||
# --rating_key {rating_key} --blur 25 --upload
|
||||
# * Watched (optional):
|
||||
# To remove the local asset episode artwork:
|
||||
# --rating_key {rating_key} --remove
|
||||
# To remove the uploaded episode artwork
|
||||
# --rating_key {rating_key} --remove --upload
|
||||
# Note:
|
||||
# * "Use local assets" must be enabled for the library in Plex (Manage Library > Edit > Advanced > Use local assets).
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import requests
|
||||
import shutil
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Environmental Variables
|
||||
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
|
||||
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN)
|
||||
|
||||
|
||||
def modify_episode_artwork(plex, rating_key, image=None, blur=None, summary_prefix=None, remove=False, upload=False):
|
||||
item = plex.fetchItem(rating_key)
|
||||
|
||||
if item.type == 'show':
|
||||
episodes = item.episodes()
|
||||
elif item.type == 'season':
|
||||
episodes = item.episodes()
|
||||
elif item.type == 'episode':
|
||||
episodes = [item]
|
||||
else:
|
||||
print('Only media type show, season, or episode is supported: '
|
||||
'{item.title} ({item.ratingKey}) is media type {item.type}.'.format(item=item))
|
||||
return
|
||||
|
||||
for episode in episodes:
|
||||
for part in episode.iterParts():
|
||||
episode_filepath = part.file
|
||||
episode_folder = os.path.dirname(episode_filepath)
|
||||
episode_filename = os.path.splitext(os.path.basename(episode_filepath))[0]
|
||||
|
||||
if remove:
|
||||
if upload:
|
||||
# Unlock and select the first poster
|
||||
episode.unlockPoster().posters()[0].select()
|
||||
else:
|
||||
# Find image files with the same name as the episode
|
||||
for filename in os.listdir(episode_folder):
|
||||
if filename.startswith(episode_filename) and filename.endswith(('.jpg', '.png')):
|
||||
# Delete the episode artwork image file
|
||||
os.remove(os.path.join(episode_folder, filename))
|
||||
|
||||
# Unlock the summary so it will get updated on refresh
|
||||
episode.editSummary(episode.summary, locked=False)
|
||||
continue
|
||||
|
||||
if image:
|
||||
if upload:
|
||||
# Upload the image to the episode artwork
|
||||
episode.uploadPoster(filepath=image)
|
||||
else:
|
||||
# File path to episode artwork using the same episode file name
|
||||
episode_artwork = os.path.splitext(episode_filepath)[0] + os.path.splitext(image)[1]
|
||||
# Copy the image to the episode artwork
|
||||
shutil.copy2(image, episode_artwork)
|
||||
|
||||
elif blur:
|
||||
# File path to episode artwork using the same episode file name
|
||||
episode_artwork = os.path.splitext(episode_filepath)[0] + '.png'
|
||||
# Get the blurred artwork
|
||||
image_url = plex.transcodeImage(
|
||||
episode.thumbUrl,
|
||||
height=270,
|
||||
width=480,
|
||||
blur=blur,
|
||||
imageFormat='png'
|
||||
)
|
||||
r = requests.get(image_url, stream=True)
|
||||
if r.status_code == 200:
|
||||
r.raw.decode_content = True
|
||||
if upload:
|
||||
# Upload the image to the episode artwork
|
||||
episode.uploadPoster(filepath=r.raw)
|
||||
else:
|
||||
# Copy the image to the episode artwork
|
||||
with open(episode_artwork, 'wb') as f:
|
||||
shutil.copyfileobj(r.raw, f)
|
||||
|
||||
if summary_prefix and not episode.summary.startswith(summary_prefix):
|
||||
# Use a zero-width space (\u200b) for blank lines
|
||||
episode.editSummary(summary_prefix + '\n\u200b\n' + episode.summary)
|
||||
|
||||
# Refresh metadata for the episode
|
||||
episode.refresh()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rating_key', required=True, type=int)
|
||||
parser.add_argument('--image')
|
||||
parser.add_argument('--blur', type=int, default=25)
|
||||
parser.add_argument('--summary_prefix', nargs='?', const='** SPOILERS **')
|
||||
parser.add_argument('--remove', action='store_true')
|
||||
parser.add_argument('--upload', action='store_true')
|
||||
opts = parser.parse_args()
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
modify_episode_artwork(plex, **vars(opts))
|
178
utility/library_growth.py
Normal file
178
utility/library_growth.py
Normal file
@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Check Plex library locations growth over time using added date.
|
||||
Check Plex, Tautulli, OS for added time, last updated, originally availableAt, played dates
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import sys
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import CONFIG
|
||||
import requests
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as plticker
|
||||
from collections import Counter
|
||||
from matplotlib import rcParams
|
||||
rcParams.update({'figure.autolayout': True})
|
||||
|
||||
PLEX_URL =''
|
||||
PLEX_TOKEN = ''
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
|
||||
# Using CONFIG file
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl')
|
||||
if not TAUTULLI_URL:
|
||||
TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
if not TAUTULLI_APIKEY:
|
||||
TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')
|
||||
|
||||
VERIFY_SSL = False
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
sections = [x for x in plex.library.sections() if x.type not in ['artist', 'photo']]
|
||||
sections_dict = {x.key: x.title for x in sections}
|
||||
|
||||
|
||||
def graph_setup():
|
||||
fig, axs = plt.subplots(3)
|
||||
fig.set_size_inches(14, 12)
|
||||
|
||||
return axs
|
||||
|
||||
|
||||
def exclusions(all_true, select, all_items):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
all_true: bool
|
||||
All of something (allLibraries, allPlaylists, allUsers)
|
||||
select: list
|
||||
List from arguments (user, playlists, libraries)
|
||||
all_items: list or dict
|
||||
List or Dictionary of all possible somethings
|
||||
|
||||
Returns
|
||||
-------
|
||||
output: list or dict
|
||||
List of what was included/excluded
|
||||
"""
|
||||
output = ''
|
||||
if isinstance(all_items, list):
|
||||
output = []
|
||||
if all_true and not select:
|
||||
output = all_items
|
||||
elif not all_true and select:
|
||||
for item in all_items:
|
||||
if isinstance(item, str):
|
||||
return select
|
||||
else:
|
||||
if item.title in select:
|
||||
output.append(item)
|
||||
elif all_true and select:
|
||||
for x in select:
|
||||
all_items.remove(x)
|
||||
output = all_items
|
||||
|
||||
elif isinstance(all_items, dict):
|
||||
output = {}
|
||||
if all_true and not select:
|
||||
output = all_items
|
||||
elif not all_true and select:
|
||||
for key, value in all_items.items():
|
||||
if value in select:
|
||||
output[key] = value
|
||||
elif all_true and select:
|
||||
for key, value in all_items.items():
|
||||
if value not in select:
|
||||
output[key] = value
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def plex_growth(section, axs):
|
||||
library = plex.library.sectionByID(section)
|
||||
|
||||
allthem = library.all()
|
||||
|
||||
allAddedAt = [x.addedAt.date() for x in allthem if x.addedAt]
|
||||
y = range(len(allAddedAt))
|
||||
axs[0].plot(sorted(allAddedAt), y)
|
||||
axs[0].set_title('Plex {} Library Growth'.format(library.title))
|
||||
|
||||
|
||||
def plex_released(section, axs):
|
||||
|
||||
library = plex.library.sectionByID(section)
|
||||
allthem = library.all()
|
||||
|
||||
originallyAvailableAt = [x.originallyAvailableAt.date().strftime('%Y')
|
||||
for x in allthem if x.originallyAvailableAt]
|
||||
counts = Counter(sorted(originallyAvailableAt))
|
||||
|
||||
axs[1].bar(list(counts.keys()), list(counts.values()))
|
||||
loc = plticker.MultipleLocator(base=5.0) # this locator puts ticks at regular intervals
|
||||
axs[1].xaxis.set_major_locator(loc)
|
||||
axs[1].set_title('Plex {} Library Released Date'.format(library.title))
|
||||
|
||||
releasedGenres = {}
|
||||
genres = []
|
||||
for x in allthem:
|
||||
if x.originallyAvailableAt:
|
||||
releaseYear = x.originallyAvailableAt.date().strftime('%Y')
|
||||
if releasedGenres.get(releaseYear):
|
||||
for genre in x.genres:
|
||||
releasedGenres[releaseYear].append(genre.tag)
|
||||
genres.append(genre.tag)
|
||||
else:
|
||||
for genre in x.genres:
|
||||
releasedGenres[releaseYear] = [genre.tag]
|
||||
genres.append(genre.tag)
|
||||
|
||||
labels = sorted(list(set(genres)))
|
||||
for year, genre in sorted(releasedGenres.items()):
|
||||
yearGenre = Counter(sorted(genre))
|
||||
genresCounts = list(yearGenre.values())
|
||||
for i in range(len(yearGenre)):
|
||||
axs[2].bar(year, genresCounts, bottom=sum(genresCounts[:i]))
|
||||
|
||||
loc = plticker.MultipleLocator(base=5.0) # this locator puts ticks at regular intervals
|
||||
axs[2].xaxis.set_major_locator(loc)
|
||||
axs[2].legend(labels, bbox_to_anchor=(0, -0.25, 1., .102), loc='lower center',
|
||||
ncol=12, mode="expand", borderaxespad=0.)
|
||||
axs[2].set_title('Plex {} Library Released Date (Genre)'.format(library.title))
|
||||
# plt.tight_layout()
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Show library growth.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--libraries', nargs='+', choices=sections_dict.values(), metavar='',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are:\n'
|
||||
'Choices: %(choices)s')
|
||||
parser.add_argument('--allLibraries', default=False, action='store_true',
|
||||
help='Select all libraries.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
# Defining libraries
|
||||
libraries = exclusions(opts.allLibraries, opts.libraries, sections_dict)
|
||||
|
||||
for library in libraries:
|
||||
library_title = sections_dict.get(library)
|
||||
print("Starting {}".format(library_title))
|
||||
graph = graph_setup()
|
||||
plex_growth(library, graph)
|
||||
plex_released(library, graph)
|
||||
plt.savefig('{}_library_growth.png'.format(library_title), bbox_inches='tight', dpi=100)
|
||||
# plt.show()
|
100
utility/lock_unlock_poster_art.py
Normal file
100
utility/lock_unlock_poster_art.py
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Automatically lock/unlock posters and artwork in a Plex.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi
|
||||
#
|
||||
# Examples:
|
||||
# Lock poster for a specific rating key:
|
||||
# python lock_unlock_poster_art.py --rating_key 12345 --lock poster
|
||||
#
|
||||
# Unlock artwork for a specific rating key:
|
||||
# python lock_unlock_poster_art.py --rating_key 12345 --unlock art
|
||||
#
|
||||
# Lock all posters in "Movies" and "TV Shows" (Note: TV show libraries include season posters/artwork):
|
||||
# python lock_unlock_poster_art.py --libraries "Movies" "TV Shows" --lock poster
|
||||
#
|
||||
# Lock all artwork in "Anime":
|
||||
# python lock_unlock_poster_art.py --libraries "Anime" --lock art
|
||||
#
|
||||
# Lock all posters and artwork in "Movies" and "TV Shows":
|
||||
# python lock_unlock_poster_art.py --libraries "Movies" "TV Shows" --lock poster --lock art
|
||||
#
|
||||
# Unlock all posters and artwork in "Music" (Note: Music libraries include album covers/artwork):
|
||||
# python lock_unlock_poster_art.py --libraries "Music" --unlock poster --unlock art
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Environmental Variables
|
||||
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
|
||||
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN)
|
||||
|
||||
|
||||
def lock_unlock(plex, rating_key=None, libraries=None, lock=None, unlock=None):
|
||||
if libraries is None:
|
||||
libraries = []
|
||||
if lock is None:
|
||||
lock = []
|
||||
if unlock is None:
|
||||
unlock = []
|
||||
|
||||
if rating_key:
|
||||
item = plex.fetchItem(rating_key)
|
||||
lock_unlock_items([item], lock, unlock)
|
||||
if item.type == 'show':
|
||||
lock_unlock_items(item.seasons(), lock, unlock)
|
||||
elif item.type == 'artist':
|
||||
lock_unlock_items(item.albums(), lock, unlock)
|
||||
|
||||
else:
|
||||
for lib in libraries:
|
||||
library = plex.library.section(lib)
|
||||
lock_unlock_library(library, lock, unlock)
|
||||
if library.type == 'show':
|
||||
lock_unlock_library(library, lock, unlock, libtype='season')
|
||||
elif library.type == 'artist':
|
||||
lock_unlock_library(library, lock, unlock, libtype='album')
|
||||
|
||||
|
||||
def lock_unlock_items(items, lock, unlock):
|
||||
for item in items:
|
||||
if 'poster' in lock:
|
||||
item.lockPoster()
|
||||
if 'art' in lock:
|
||||
item.lockArt()
|
||||
if 'poster' in unlock:
|
||||
item.unlockPoster()
|
||||
if 'art' in unlock:
|
||||
item.unlockArt()
|
||||
|
||||
|
||||
def lock_unlock_library(library, lock, unlock, libtype=None):
|
||||
if 'poster' in lock:
|
||||
library.lockAllField('thumb', libtype=libtype)
|
||||
if 'art' in lock:
|
||||
library.lockAllField('art', libtype=libtype)
|
||||
if 'poster' in unlock:
|
||||
library.unlockAllField('thumb', libtype=libtype)
|
||||
if 'art' in unlock:
|
||||
library.unlockAllField('art', libtype=libtype)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
sections = [library.title for library in plex.library.sections()]
|
||||
lock_options = {'poster', 'art'}
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rating_key', type=int)
|
||||
parser.add_argument('--libraries', nargs='+', choices=sections)
|
||||
parser.add_argument('--lock', choices=lock_options, action='append')
|
||||
parser.add_argument('--unlock', choices=lock_options, action='append')
|
||||
opts = parser.parse_args()
|
||||
|
||||
lock_unlock(plex, **vars(opts))
|
49
utility/mark_multiepisode_watched.py
Normal file
49
utility/mark_multiepisode_watched.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Automatically mark a multi-episode file as watched in Plex.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi
|
||||
# Tautulli script trigger:
|
||||
# * Notify on watched
|
||||
# Tautulli script conditions:
|
||||
# * Condition {1}:
|
||||
# [ Media Type | is | episode ]
|
||||
# * Condition {2} (optional):
|
||||
# [ Username | is | username ]
|
||||
# Tautulli script arguments:
|
||||
# * Watched:
|
||||
# --rating_key {rating_key} --filename {filename}
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from builtins import str
|
||||
import argparse
|
||||
import os
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Environmental Variables
|
||||
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
|
||||
PLEX_USER_TOKEN = os.getenv('PLEX_USER_TOKEN', PLEX_TOKEN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rating_key', required=True, type=int)
|
||||
parser.add_argument('--filename', required=True)
|
||||
opts = parser.parse_args()
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_USER_TOKEN)
|
||||
|
||||
for episode in plex.fetchItem(opts.rating_key).season().episodes():
|
||||
if episode.ratingKey == opts.rating_key:
|
||||
continue
|
||||
if any(opts.filename in part.file for media in episode.media for part in media.parts):
|
||||
print("Marking multi-episode file '{grandparentTitle} - S{parentIndex}E{index}' as watched.".format(
|
||||
grandparentTitle=episode.grandparentTitle.encode('UTF-8'),
|
||||
parentIndex=str(episode.parentIndex).zfill(2),
|
||||
index=str(episode.index).zfill(2)))
|
||||
episode.markWatched()
|
834
utility/media_manager.py
Normal file
834
utility/media_manager.py
Normal file
@ -0,0 +1,834 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Manage Plex media.
|
||||
Show, delete, archive, optimize, or move media based on whether it was
|
||||
watched, unwatched, transcoded often, or file size is greater than X
|
||||
|
||||
*Tautulli data to command Plex
|
||||
|
||||
Author: Blacktwin
|
||||
Requires: requests, plexapi, argparse
|
||||
Interacts with: Tautulli, Plex
|
||||
|
||||
Enabling Scripts in Tautulli:
|
||||
Not yet
|
||||
|
||||
Examples:
|
||||
Find unwatched Movies that were added before 2015-05-05 and delete
|
||||
python media_manager.py --libraries Movies --select unwatched --date "2015-05-05" --action delete
|
||||
|
||||
Find watched TV Shows that both User1 and User2 have watched
|
||||
python media_manager.py --libraries "TV Shows" --select watched --users User1 User2 --action show
|
||||
|
||||
Find library items that were last played before 2021-01-01
|
||||
python media_manger.py --libraries "Movies --select lastPlayed --date 2021-01-01 --action show
|
||||
|
||||
Find library items that have audience rating less than 6
|
||||
python3 media_manager.py --libraries "Movies 2022" --select rating --selectValue "<_6" --action show
|
||||
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from builtins import object
|
||||
import argparse
|
||||
import datetime
|
||||
import time
|
||||
import re
|
||||
from collections import Counter
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import CONFIG
|
||||
from plexapi.exceptions import NotFound
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
PLEX_URL =''
|
||||
PLEX_TOKEN = ''
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
|
||||
# Using CONFIG file
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl')
|
||||
if not TAUTULLI_URL:
|
||||
TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
if not TAUTULLI_APIKEY:
|
||||
TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')
|
||||
|
||||
VERIFY_SSL = False
|
||||
|
||||
SELECTOR = ['watched', 'unwatched', 'transcoded', 'rating', 'size', 'lastPlayed']
|
||||
ACTIONS = ['delete', 'move', 'archive', 'optimize', 'show']
|
||||
OPERATORS = { '>': lambda v, q: v > q,
|
||||
'>=': lambda v, q: v >= q,
|
||||
'<': lambda v, q: v < q,
|
||||
'<=': lambda v, q: v <= q,}
|
||||
|
||||
UNTIS = {"B": 1, "KB": 2**10, "MB": 2**20, "GB": 2**30, "TB": 2**40}
|
||||
|
||||
MOVE_PATH = ''
|
||||
ARCHIVE_PATH = ''
|
||||
OPTIMIZE_DEFAULT = {'targetTagID': 'Mobile',
|
||||
'deviceProfile': None,
|
||||
'title': None,
|
||||
'target': "",
|
||||
'locationID': -1,
|
||||
'policyUnwatched': 0,
|
||||
'videoQuality': None}
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, url=None, apikey=None, verify_ssl=False):
|
||||
self.url = url
|
||||
self.apikey = apikey
|
||||
self.session = Session()
|
||||
self.adapters = HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session.mount('http://', self.adapters)
|
||||
self.session.mount('https://', self.adapters)
|
||||
|
||||
# Ignore verifying the SSL certificate
|
||||
if verify_ssl is False:
|
||||
self.session.verify = False
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
class Library(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.title = d['section_name']
|
||||
self.key = d['section_id']
|
||||
self.type = d['section_type']
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.added_at = d.get('added_at')
|
||||
self.media_type = d.get('media_type')
|
||||
self.grandparent_title = d.get('grandparent_title')
|
||||
self.grandparent_rating_key = d.get('grandparent_rating_key')
|
||||
self.parent_media_index = d.get('parent_media_index')
|
||||
self.parent_title = d.get('parent_title')
|
||||
self.parent_rating_key = d.get('parent_rating_key')
|
||||
self.file_size = d.get('file_size')
|
||||
self.container = d.get('container')
|
||||
self.rating_key = d.get('rating_key')
|
||||
self.index = d.get('media_index')
|
||||
self.watched_status = d.get('watched_status')
|
||||
self.libraryName = d.get("library_name")
|
||||
self.full_title = d.get('full_title')
|
||||
self.title = d.get('title')
|
||||
self.year = d.get('year')
|
||||
self.video_resolution = d.get('video_resolution')
|
||||
self.video_codec = d.get('video_codec')
|
||||
self.media_info = d.get('media_info')
|
||||
self.audience_rating= d.get('audience_rating')
|
||||
if self.media_info:
|
||||
self.parts = self.media_info[0].get('parts')
|
||||
self.file = self.parts[0].get('file')
|
||||
if not self.file_size:
|
||||
self.file_size = self.parts[0].get('file_size')
|
||||
if self.media_type == 'episode' and not self.title:
|
||||
episodeName = self.full_title.partition('-')[-1]
|
||||
self.title = episodeName.lstrip()
|
||||
elif not self.title:
|
||||
self.title = self.full_title
|
||||
|
||||
if self.media_type == 'show':
|
||||
show = plex.fetchItem(int(self.rating_key))
|
||||
# todo only using the first library location for show types
|
||||
self.file = show.locations[0]
|
||||
show = tautulli_server.get_new_rating_keys(self.rating_key, self.media_type)
|
||||
seasons = show['0']['children']
|
||||
episodes = []
|
||||
show_size = []
|
||||
for season in seasons.values():
|
||||
for _episode in season['children'].values():
|
||||
metadata = tautulli_server.get_metadata(_episode['rating_key'])
|
||||
episode = Metadata(metadata)
|
||||
show_size.append(int(episode.file_size))
|
||||
episodes.append(episode)
|
||||
self.file_size = sum(show_size)
|
||||
self.episodes = episodes
|
||||
|
||||
|
||||
class User(object):
|
||||
def __init__(self, name='', email='', userid='',):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.userid = userid
|
||||
self.watch = {}
|
||||
self.transcode = {}
|
||||
self.direct = {}
|
||||
|
||||
|
||||
class Tautulli(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def _call_api(self, cmd, payload, method='GET'):
|
||||
payload['cmd'] = cmd
|
||||
payload['apikey'] = self.connection.apikey
|
||||
|
||||
try:
|
||||
response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload)
|
||||
except RequestException as e:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
|
||||
return
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
except ValueError:
|
||||
print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd))
|
||||
return
|
||||
|
||||
if response_json['response']['result'] == 'success':
|
||||
return response_json['response']['data']
|
||||
else:
|
||||
error_msg = response_json['response']['message']
|
||||
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
|
||||
return
|
||||
|
||||
def get_history(self, user=None, section_id=None, rating_key=None, start=None, length=None, watched=None,
|
||||
transcode_decision=None):
|
||||
"""Call Tautulli's get_history api endpoint."""
|
||||
payload = {"order_column": "full_title",
|
||||
"order_dir": "asc"}
|
||||
|
||||
watched_status = None
|
||||
if watched is True:
|
||||
watched_status = 1
|
||||
if watched is False:
|
||||
watched_status = 0
|
||||
|
||||
if user:
|
||||
payload["user"] = user
|
||||
if section_id:
|
||||
payload["section_id"] = section_id
|
||||
if rating_key:
|
||||
payload["rating_key"] = rating_key
|
||||
if start:
|
||||
payload["start"] = start
|
||||
if length:
|
||||
payload["lengh"] = length
|
||||
if transcode_decision:
|
||||
payload["transcode_decision"] = transcode_decision
|
||||
|
||||
history = self._call_api('get_history', payload)
|
||||
|
||||
if isinstance(watched_status, int):
|
||||
return [d for d in history['data'] if d['watched_status'] == watched_status]
|
||||
else:
|
||||
return [d for d in history['data']]
|
||||
|
||||
def get_metadata(self, rating_key):
|
||||
"""Call Tautulli's get_metadata api endpoint."""
|
||||
payload = {"rating_key": rating_key}
|
||||
return self._call_api('get_metadata', payload)
|
||||
|
||||
def get_libraries(self):
|
||||
"""Call Tautulli's get_libraries api endpoint."""
|
||||
payload = {}
|
||||
return self._call_api('get_libraries', payload)
|
||||
|
||||
def get_library_media_info(self, section_id, start, length, unwatched=None, date=None, order_column=None,
|
||||
last_played=None):
|
||||
"""Call Tautulli's get_library_media_info api endpoint."""
|
||||
payload = {'section_id': section_id}
|
||||
if start:
|
||||
payload["start"] = start
|
||||
if length:
|
||||
payload["lengh"] = length
|
||||
if order_column:
|
||||
payload["order_column"] = order_column
|
||||
payload['order_dir'] = 'desc'
|
||||
|
||||
library_stats = self._call_api('get_library_media_info', payload)
|
||||
if unwatched and not date:
|
||||
return [d for d in library_stats['data'] if d['play_count'] is None]
|
||||
elif unwatched and date:
|
||||
return [d for d in library_stats['data'] if d['play_count'] is None
|
||||
and (float(d['added_at'])) < date]
|
||||
elif last_played and date:
|
||||
return [d for d in library_stats['data'] if d['play_count'] is not None]
|
||||
else:
|
||||
return [d for d in library_stats['data']]
|
||||
|
||||
def get_new_rating_keys(self, rating_key, media_type):
|
||||
"""Call Tautulli's get_new_rating_keys api endpoint."""
|
||||
payload = {"rating_key": rating_key, "media_type": media_type}
|
||||
return self._call_api('get_new_rating_keys', payload)
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
# Function found https://stackoverflow.com/a/1094933
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||
|
||||
|
||||
def parseSize(size):
|
||||
size = size.upper()
|
||||
if not re.match(r' ', size):
|
||||
size = re.sub(r'([KMGT]?B)', r' \1', size)
|
||||
number, unit = [string.strip() for string in size.split()]
|
||||
return int(float(number)*UNTIS[unit])
|
||||
|
||||
|
||||
def plex_deletion(items, libraries, toggleDeletion):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
items (list): List of items to be deleted by Plex
|
||||
libraries {list): List of libraries used
|
||||
toggleDeletion (bool): Allow then disable Plex ability to delete media items
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
if plex.allowMediaDeletion is None and toggleDeletion is None:
|
||||
print("Allow Plex to delete media.")
|
||||
exit()
|
||||
elif plex.allowMediaDeletion is None and toggleDeletion:
|
||||
print("Temporarily allowing Plex to delete media.")
|
||||
plex._allowMediaDeletion(True)
|
||||
time.sleep(1)
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
print("The following items were added before {} and marked for deletion.".format(opts.date))
|
||||
for item in items:
|
||||
try:
|
||||
if isinstance(item, int):
|
||||
plex_item = plex.fetchItem(item)
|
||||
elif isinstance(item, str):
|
||||
plex_item = plex.fetchItem(int(item))
|
||||
else:
|
||||
plex_item = plex.fetchItem(int(item.rating_key))
|
||||
plex_item.delete()
|
||||
print("Item: {} was deleted".format(plex_item.title))
|
||||
except NotFound:
|
||||
print("Item: {} may already have been deleted.".format(item))
|
||||
for _library in libraries:
|
||||
section = plex.library.sectionByID(_library.key)
|
||||
print("Emptying Trash from library {}".format(_library.title))
|
||||
section.emptyTrash()
|
||||
if toggleDeletion:
|
||||
print("Disabling Plex to delete media.")
|
||||
plex._allowMediaDeletion(False)
|
||||
|
||||
|
||||
def last_played_work(sectionID, date=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
sectionID (int): Library key
|
||||
date (float): Epoch time
|
||||
|
||||
Returns
|
||||
-------
|
||||
last_played_lst (list): List of Metdata objects of last played items
|
||||
"""
|
||||
count = 25
|
||||
start = 0
|
||||
last_played_lst = []
|
||||
while True:
|
||||
|
||||
# Getting all watched history
|
||||
tt_history = tautulli_server.get_library_media_info(section_id=sectionID,
|
||||
start=start, length=count, last_played=True,
|
||||
date=date, order_column='last_played')
|
||||
|
||||
if all([tt_history]):
|
||||
start += count
|
||||
for item in tt_history:
|
||||
_meta = tautulli_server.get_metadata(item['rating_key'])
|
||||
if _meta: # rating_key that no longer exists on the Plex server will return blank metadata
|
||||
metadata = Metadata(_meta)
|
||||
if (float(item['last_played'])) < date:
|
||||
metadata.last_played = item['last_played']
|
||||
last_played_lst.append(metadata)
|
||||
elif not all([tt_history]):
|
||||
break
|
||||
start += count
|
||||
|
||||
return last_played_lst
|
||||
|
||||
|
||||
def unwatched_work(sectionID, date=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
sectionID (int): Library key
|
||||
date (float): Epoch time
|
||||
|
||||
Returns
|
||||
-------
|
||||
unwatched_lst (list): List of Metdata objects of unwatched items
|
||||
"""
|
||||
count = 25
|
||||
start = 0
|
||||
unwatched_lst = []
|
||||
while True:
|
||||
|
||||
# Getting all watched history for userFrom
|
||||
tt_history = tautulli_server.get_library_media_info(section_id=sectionID,
|
||||
start=start, length=count, unwatched=True, date=date)
|
||||
|
||||
if all([tt_history]):
|
||||
start += count
|
||||
for item in tt_history:
|
||||
_meta = tautulli_server.get_metadata(item['rating_key'])
|
||||
if _meta: # rating_key that no longer exists on the Plex server will return blank metadata
|
||||
metadata = Metadata(_meta)
|
||||
unwatched_lst.append(metadata)
|
||||
continue
|
||||
elif not all([tt_history]):
|
||||
break
|
||||
start += count
|
||||
|
||||
return unwatched_lst
|
||||
|
||||
|
||||
def size_work(sectionID, operator, value, episodes):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
sectionID (int): Library key
|
||||
date (float): Epoch time
|
||||
|
||||
Returns
|
||||
-------
|
||||
unwatched_lst (list): List of Metdata objects of unwatched items
|
||||
"""
|
||||
count = 25
|
||||
start = 0
|
||||
size_lst = []
|
||||
while True:
|
||||
|
||||
# Getting all watched history for userFrom
|
||||
tt_size = tautulli_server.get_library_media_info(section_id=sectionID,
|
||||
start=start, length=count,
|
||||
order_column="file_size")
|
||||
if all([tt_size]):
|
||||
start += count
|
||||
for item in tt_size:
|
||||
_meta = tautulli_server.get_metadata(item['rating_key'])
|
||||
if _meta: # rating_key that no longer exists on the Plex server will return blank metadata
|
||||
metadata = Metadata(_meta)
|
||||
try:
|
||||
if episodes:
|
||||
for _episode in metadata.episodes:
|
||||
file_size = int(_episode.file_size)
|
||||
if operator(file_size, value):
|
||||
size_lst.append(_episode)
|
||||
else:
|
||||
file_size = int(metadata.file_size)
|
||||
if operator(file_size, value):
|
||||
size_lst.append(metadata)
|
||||
except AttributeError:
|
||||
print("Metadata error found with rating_key: ({})".format(item['rating_key']))
|
||||
continue
|
||||
elif not all([tt_size]):
|
||||
break
|
||||
start += count
|
||||
return size_lst
|
||||
|
||||
|
||||
def watched_work(user, sectionID=None, ratingKey=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
user (object): User object holding user stats
|
||||
sectionID {int): Library key
|
||||
ratingKey (int): Item rating key
|
||||
|
||||
-------
|
||||
"""
|
||||
count = 25
|
||||
start = 0
|
||||
tt_history = ''
|
||||
|
||||
while True:
|
||||
|
||||
# Getting all watched history for userFrom
|
||||
if sectionID:
|
||||
tt_history = tautulli_server.get_history(user=user.name, section_id=sectionID,
|
||||
start=start, length=count, watched=True)
|
||||
elif ratingKey:
|
||||
tt_history = tautulli_server.get_history(user=user.name, rating_key=ratingKey,
|
||||
start=start, length=count, watched=True)
|
||||
|
||||
if all([tt_history]):
|
||||
start += count
|
||||
for item in tt_history:
|
||||
metadata = Metadata(item)
|
||||
if user.watch.get(metadata.rating_key):
|
||||
user.watch.get(metadata.rating_key).watched_status += 1
|
||||
else:
|
||||
_meta = tautulli_server.get_metadata(metadata.rating_key)
|
||||
if _meta: # rating_key that no longer exists on the Plex server will return blank metadata
|
||||
metadata = Metadata(_meta)
|
||||
user.watch.update({metadata.rating_key: metadata})
|
||||
|
||||
continue
|
||||
elif not all([tt_history]):
|
||||
break
|
||||
start += count
|
||||
|
||||
def rating_work(sectionID, operator, value):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
sectionID (int): Library key
|
||||
value (str): audience rating criteria
|
||||
Returns
|
||||
-------
|
||||
rating_lst (list): List of Metdata objects of items matching audience rating
|
||||
"""
|
||||
count = 25
|
||||
start = 0
|
||||
rating_lst = []
|
||||
while True:
|
||||
tt_size = tautulli_server.get_library_media_info(section_id=sectionID,
|
||||
start=start, length=count)
|
||||
if all([tt_size]):
|
||||
start += count
|
||||
for item in tt_size:
|
||||
_meta = tautulli_server.get_metadata(item['rating_key'])
|
||||
if _meta: # rating_key that no longer exists on the Plex server will return blank metadata
|
||||
metadata = Metadata(_meta)
|
||||
try:
|
||||
if metadata.audience_rating:
|
||||
audience_rating = float(metadata.audience_rating)
|
||||
if operator(audience_rating, float(value)):
|
||||
rating_lst.append(metadata)
|
||||
except AttributeError:
|
||||
print("Metadata error found with rating_key: ({})".format(item['rating_key']))
|
||||
continue
|
||||
elif not all([tt_size]):
|
||||
break
|
||||
start += count
|
||||
return rating_lst
|
||||
|
||||
def transcode_work(sectionID, operator, value):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
user (object): User object holding user stats
|
||||
sectionID {int): Library key
|
||||
ratingKey (int): Item rating key
|
||||
|
||||
-------
|
||||
"""
|
||||
count = 25
|
||||
start = 0
|
||||
transcoding_lst = []
|
||||
transcoding_count = {}
|
||||
|
||||
while True:
|
||||
|
||||
# Getting all watched history for userFrom
|
||||
tt_history = tautulli_server.get_history(section_id=sectionID, start=start, length=count,
|
||||
transcode_decision="transcode")
|
||||
|
||||
if all([tt_history]):
|
||||
start += count
|
||||
for item in tt_history:
|
||||
if transcoding_count.get(item['rating_key']):
|
||||
transcoding_count[item['rating_key']] += 1
|
||||
else:
|
||||
transcoding_count[item['rating_key']] = 1
|
||||
|
||||
continue
|
||||
elif not all([tt_history]):
|
||||
break
|
||||
start += count
|
||||
|
||||
sorted_transcoding = sorted(transcoding_count.items(), key=lambda x: x[1], reverse=True)
|
||||
for rating_key, transcode_count in sorted_transcoding:
|
||||
if operator(transcode_count, int(value)):
|
||||
_meta = tautulli_server.get_metadata(rating_key)
|
||||
if _meta:
|
||||
metadata = Metadata(_meta)
|
||||
metadata.transcode_count = transcode_count
|
||||
transcoding_lst.append(metadata)
|
||||
else:
|
||||
print("Metadata error found with rating_key: ({})".format(rating_key))
|
||||
|
||||
|
||||
return transcoding_lst
|
||||
|
||||
|
||||
def action_show(items, selector, date, users=None):
|
||||
sizes = []
|
||||
|
||||
print("{} item(s) have been found.".format(len(items)))
|
||||
if selector == 'lastPlayed':
|
||||
print("The following items were last played before {}".format(date))
|
||||
elif selector == 'watched':
|
||||
print("The following items were watched by {}".format(", ".join([user.name for user in users])))
|
||||
elif selector == 'unwatched':
|
||||
print("The following items were added before {} and are unwatched".format(date))
|
||||
elif selector == 'rating':
|
||||
print("The following item(s) meet the criteria")
|
||||
else:
|
||||
print("The following items were added before {}".format(date))
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
if selector == 'watched':
|
||||
item = users[0].watch[item]
|
||||
added_at = datetime.datetime.utcfromtimestamp(float(item.added_at)).strftime("%Y-%m-%d")
|
||||
size = int(item.file_size) if item.file_size else 0
|
||||
sizes.append(size)
|
||||
|
||||
if selector == 'lastPlayed':
|
||||
last_played = datetime.datetime.utcfromtimestamp(float(item.last_played)).strftime("%Y-%m-%d")
|
||||
print(u"\t{} added {} and last played {}\tSize: {}\n\t\tFile: {}".format(
|
||||
item.title, added_at, last_played, sizeof_fmt(size), item.file))
|
||||
|
||||
elif selector == 'rating':
|
||||
print(u"\t{} added {}\tSize: {}\tRating: {}\n\t\tFile: {}".format(
|
||||
item.title, added_at, sizeof_fmt(size), item.audience_rating, item.file))
|
||||
|
||||
elif selector == 'transcoded':
|
||||
print(u"\t{} added {}\tSize: {}\tTransocded: {} time(s)\n\t\tFile: {}".format(
|
||||
item.title, added_at, sizeof_fmt(size), item.transcode_count, item.file))
|
||||
|
||||
else:
|
||||
print(u"\t{} added {}\tSize: {}\n\t\tFile: {}".format(
|
||||
item.title, added_at, sizeof_fmt(size), item.file))
|
||||
|
||||
except TypeError as e:
|
||||
print("Item: {} caused the following error: {}".format(item.rating_key, e))
|
||||
|
||||
total_size = sum(sizes)
|
||||
print("Total size: {}".format(sizeof_fmt(total_size)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
session = Connection().session
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=session)
|
||||
all_users = plex.myPlexAccount().users()
|
||||
all_users.append(plex.myPlexAccount())
|
||||
users = {user.title: User(name=user.title, email=user.email, userid=user.id)
|
||||
for user in all_users}
|
||||
|
||||
user_choices = []
|
||||
for user in users.values():
|
||||
if user.email:
|
||||
user_choices.append(user.email)
|
||||
user_choices.append(user.userid)
|
||||
user_choices.append(user.name)
|
||||
sections_lst = [x.title for x in plex.library.sections()]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Manage Plex media using data captured from Tautulli.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--select', required=True, choices=SELECTOR,
|
||||
help='Select what kind of items to look for.\nChoices: (%(choices)s)')
|
||||
parser.add_argument('--action', required=True, choices=ACTIONS,
|
||||
help='Action to perform with items collected.\nChoices: (%(choices)s)')
|
||||
parser.add_argument('--libraries', nargs='+', choices=sections_lst, metavar='',
|
||||
help='Libraries to scan for watched/unwatched content.')
|
||||
parser.add_argument('--ratingKey', nargs="?", type=str,
|
||||
help='Rating key of item to scan for watched/unwatched status.')
|
||||
parser.add_argument('--date', nargs="?", type=str, default=None,
|
||||
help='Check items added before YYYY-MM-DD for watched/unwatched status.')
|
||||
parser.add_argument('--users', nargs='+', choices=user_choices, metavar='',
|
||||
help='Plex usernames, userid, or email of users to use. Allowed names are:\n'
|
||||
'Choices: %(choices)s')
|
||||
parser.add_argument('--notify', type=int,
|
||||
help='Notification Agent ID number to Agent to ' +
|
||||
'send notification.')
|
||||
parser.add_argument('--toggleDeletion', action='store_true',
|
||||
help='Enable Plex to delete media while using script.')
|
||||
parser.add_argument('--actionOption', type=lambda kv: kv.split("="), action='append',
|
||||
help='Addtional instructions to use for move, archive, optimize.\n'
|
||||
'--action optimize --actionOption title="Optimized thing"\n'
|
||||
'--action optimize --actionOption targetTagID=Mobile\n'
|
||||
'--action move --actionOption path="D:/my/new/path"')
|
||||
parser.add_argument('--selectValue', type=lambda kv: kv.split("_"),
|
||||
help='Operator and Value to use for size, rating or transcoded filtering.\n'
|
||||
'">_5G" ie. items greater than 5 gigabytes.\n'
|
||||
'">_3" ie. items greater than 3 stars.\n'
|
||||
'">_3" ie. items played transcoded more than 3 times.')
|
||||
parser.add_argument('--episodes', action='store_true',
|
||||
help='Enable Plex to scan episodes if Show library is selected.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
# todo find: watched by list of users[x], unwatched based on time[x], based on size, most transcoded, star rating
|
||||
# todo find: all selectors should be able to search by user, library, and/or time
|
||||
# todo actions: delete[x], move?, zip and move?, notify, optimize
|
||||
# todo deletion toggle and optimize is dependent on plexapi PRs 433 and 426 respectively
|
||||
# todo logging and notification
|
||||
# todo if optimizing and optimized version already exists, skip
|
||||
|
||||
libraries = []
|
||||
all_sections = []
|
||||
watched_lst = []
|
||||
unwatched_lst = []
|
||||
last_played_lst = []
|
||||
size_lst = []
|
||||
user_lst = []
|
||||
rating_lst = []
|
||||
transcode_lst = []
|
||||
date_format = ''
|
||||
|
||||
# Check for days or date format
|
||||
if opts.date and opts.date.isdigit():
|
||||
days = datetime.date.today() - datetime.timedelta(int(opts.date))
|
||||
date = time.mktime(days.timetuple())
|
||||
elif opts.date is None:
|
||||
date = None
|
||||
else:
|
||||
date = time.mktime(time.strptime(opts.date, "%Y-%m-%d"))
|
||||
|
||||
if date:
|
||||
days = (datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(date))
|
||||
date_format = time.strftime("%Y-%m-%d", time.localtime(date))
|
||||
date_format = '{} ({} days)'.format(date_format, days.days)
|
||||
# Create a Tautulli instance
|
||||
tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip("/"),
|
||||
apikey=TAUTULLI_APIKEY,
|
||||
verify_ssl=VERIFY_SSL))
|
||||
|
||||
# Pull all libraries from Tautulli
|
||||
_sections = {}
|
||||
tautulli_sections = tautulli_server.get_libraries()
|
||||
for section in tautulli_sections:
|
||||
section_obj = Library(section)
|
||||
_sections[section_obj.title] = section_obj
|
||||
all_sections = _sections
|
||||
|
||||
# Defining libraries
|
||||
if opts.libraries:
|
||||
for library in opts.libraries:
|
||||
if all_sections.get(library):
|
||||
libraries.append(all_sections.get(library))
|
||||
else:
|
||||
print("No matching library name '{}'".format(library))
|
||||
exit()
|
||||
|
||||
if opts.users:
|
||||
for _user in opts.users:
|
||||
user_lst.append(users[_user])
|
||||
|
||||
if opts.select == "unwatched":
|
||||
if libraries:
|
||||
for _library in libraries:
|
||||
print("Checking library: '{}' watch statuses...".format(_library.title))
|
||||
unwatched_lst += unwatched_work(sectionID=_library.key, date=date)
|
||||
if not unwatched_lst:
|
||||
print("{} item(s) have been found.".format(len(unwatched_lst)))
|
||||
exit()
|
||||
if opts.action == "show":
|
||||
action_show(unwatched_lst, opts.select, date_format)
|
||||
|
||||
if opts.action == "delete":
|
||||
plex_deletion(unwatched_lst, libraries, opts.toggleDeletion)
|
||||
|
||||
if opts.select == "watched":
|
||||
if libraries:
|
||||
for user in user_lst:
|
||||
print("Finding watched items from user: {}".format(user.name))
|
||||
for _library in libraries:
|
||||
print("Checking library: '{}' watch statuses...".format(_library.title))
|
||||
watched_work(user=user, sectionID=_library.key)
|
||||
|
||||
if opts.ratingKey:
|
||||
item = tautulli_server.get_metadata(rating_key=opts.ratingKey)
|
||||
metadata = Metadata(item)
|
||||
if metadata.media_type in ['show', 'season']:
|
||||
parent = plex.fetchItem(int(opts.ratingKey))
|
||||
childern = parent.episodes()
|
||||
for user in user_lst:
|
||||
for child in childern:
|
||||
watched_work(user=user, ratingKey=child.ratingKey)
|
||||
else:
|
||||
for user in user_lst:
|
||||
watched_work(user=user, ratingKey=opts.ratingKey)
|
||||
|
||||
# Find all items watched by all users
|
||||
all_watched = [key for user in user_lst for key in user.watch.keys() if key is not None]
|
||||
counts = Counter(all_watched)
|
||||
watched_by_all = [id for id in all_watched if counts[id] >= len(user_lst)]
|
||||
watched_by_all = list(set(watched_by_all))
|
||||
|
||||
if opts.action == "show":
|
||||
action_show(watched_by_all, opts.select, date_format, user_lst)
|
||||
|
||||
if opts.action == "delete":
|
||||
plex_deletion(watched_by_all, libraries, opts.toggleDeletion)
|
||||
|
||||
if opts.select == "lastPlayed":
|
||||
if libraries:
|
||||
for _library in libraries:
|
||||
print("Checking library: '{}' watch statuses...".format(_library.title))
|
||||
last_played_lst += last_played_work(sectionID=_library.key, date=date)
|
||||
|
||||
if opts.action == "show":
|
||||
action_show(last_played_lst, opts.select, date_format)
|
||||
|
||||
if opts.action == "delete":
|
||||
plex_deletion(last_played_lst, libraries, opts.toggleDeletion)
|
||||
|
||||
if opts.select in ["size", "rating", "transcoded"]:
|
||||
if opts.selectValue:
|
||||
operator, value = opts.selectValue
|
||||
if operator not in OPERATORS.keys():
|
||||
print("Operator not found")
|
||||
exit()
|
||||
else:
|
||||
print("No value provided.")
|
||||
exit()
|
||||
|
||||
op = OPERATORS.get(operator)
|
||||
|
||||
if opts.select == "size":
|
||||
if value[-2:] in UNTIS.keys():
|
||||
size = parseSize(value)
|
||||
if libraries:
|
||||
for _library in libraries:
|
||||
print("Checking library: '{}' items {}{} in size...".format(_library.title, operator, value))
|
||||
size_lst += size_work(sectionID=_library.key, operator=op, value=size, episodes=opts.episodes)
|
||||
|
||||
if opts.action == "show":
|
||||
action_show(size_lst, opts.select, opts.date)
|
||||
else:
|
||||
print("Size must end with one of these notations: {}".format(", ".join(UNTIS.keys())))
|
||||
pass
|
||||
elif opts.select == "rating":
|
||||
if libraries:
|
||||
for _library in libraries:
|
||||
print("Checking library: '{}' items with {}{} rating...".format(
|
||||
_library.title, operator, value))
|
||||
rating_lst += rating_work(sectionID=_library.key, operator=op, value=value)
|
||||
|
||||
if opts.action == "show":
|
||||
action_show(rating_lst, opts.select, None)
|
||||
elif opts.action == 'delete':
|
||||
plex_deletion(rating_lst, libraries, opts.toggleDeletion)
|
||||
elif opts.select == "transcoded":
|
||||
if libraries:
|
||||
for _library in libraries:
|
||||
print("Checking library: '{}' items with {}{} transcodes...".format(
|
||||
_library.title, operator, value))
|
||||
transcoded_lst = transcode_work(sectionID=_library.key, operator=op, value=value)
|
||||
transcode_lst += transcoded_lst
|
||||
|
||||
if opts.action == "show":
|
||||
action_show(transcode_lst, opts.select, date_format)
|
||||
|
216
utility/merge_multiepisodes.py
Normal file
216
utility/merge_multiepisodes.py
Normal file
@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Description: Automatically merge multi-episode files in Plex into a single entry.
|
||||
Author: /u/SwiftPanda16
|
||||
Requires: plexapi, pillow (optional)
|
||||
Notes:
|
||||
* All episodes **MUST** be organized correctly according to Plex's "Multiple Episodes in a Single File".
|
||||
https://support.plex.tv/articles/naming-and-organizing-your-tv-show-files/#toc-4
|
||||
|
||||
* Episode titles, summaries, and tags will be appended to the first episode of the group.
|
||||
|
||||
* Without re-numbering will keep the episode number of the first episode of each group.
|
||||
|
||||
* Re-numbering starts at the first group episode's number and increments by one. Skipping numbers is not supported.
|
||||
* e.g. s01e01-e02, s01e03, s01e04, s01e05-e06 --> s01e01, s01e02, s01e03, s01e04
|
||||
* e.g. s02e05-e06, s01e07-e08, s02e09-e10 --> s02e05, s02e06, s02e07
|
||||
* e.g. s03e01-e02, s03e04, s03e07-e08 --> s03e01, s03e02, s03e03 (s03e03, s03e05, so3e06 skipped)
|
||||
|
||||
* To revert the changes and split the episodes again, the show must be removed and re-added to Plex (aka Plex Dance).
|
||||
|
||||
Usage:
|
||||
* Without renumbering episodes:
|
||||
python merge_multiepisodes.py --library "TV Shows" --show "SpongeBob SquarePants"
|
||||
|
||||
* With renumbering episodes:
|
||||
python merge_multiepisodes.py --library "TV Shows" --show "SpongeBob SquarePants" --renumber
|
||||
|
||||
* With renumbering episodes and composite thumb:
|
||||
python merge_multiepisodes.py --library "TV Shows" --show "SpongeBob SquarePants" --renumber --composite-thumb
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import requests
|
||||
from collections import defaultdict
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw
|
||||
hasPIL = True
|
||||
except ImportError:
|
||||
hasPIL = False
|
||||
|
||||
|
||||
# ## EDIT SETTINGS ##
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Composite Thumb Settings
|
||||
WIDTH, HEIGHT = 640, 360 # 16:9 aspect ratio
|
||||
LINE_ANGLE = 25 # degrees
|
||||
LINE_THICKNESS = 10
|
||||
|
||||
|
||||
# Environmental Variables
|
||||
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
|
||||
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN)
|
||||
|
||||
|
||||
def group_episodes(plex, library, show, renumber, composite_thumb):
|
||||
show = plex.library.section(library).get(show)
|
||||
|
||||
for season in show.seasons():
|
||||
groups = defaultdict(list)
|
||||
startIndex = None
|
||||
|
||||
for episode in season.episodes():
|
||||
groups[episode.locations[0]].append(episode)
|
||||
if startIndex is None:
|
||||
startIndex = episode.index
|
||||
|
||||
for index, (first, *episodes) in enumerate(groups.values(), start=startIndex):
|
||||
title = first.title + ' / '
|
||||
titleSort = first.titleSort + ' / '
|
||||
summary = first.summary + '\n\n'
|
||||
writers = []
|
||||
directors = []
|
||||
|
||||
for episode in episodes:
|
||||
title += episode.title + ' / '
|
||||
titleSort += episode.titleSort + ' / '
|
||||
summary += episode.summary + '\n\n'
|
||||
writers.extend([writer.tag for writer in episode.writers])
|
||||
directors.extend([director.tag for director in episode.directors])
|
||||
|
||||
if episodes:
|
||||
if composite_thumb:
|
||||
firstImgFile = download_image(
|
||||
plex.transcodeImage(first.thumbUrl, width=WIDTH, height=HEIGHT)
|
||||
)
|
||||
lastImgFile = download_image(
|
||||
plex.transcodeImage(episodes[-1].thumbUrl, width=WIDTH, height=HEIGHT)
|
||||
)
|
||||
compImgFile = create_composite_thumb(firstImgFile, lastImgFile)
|
||||
first.uploadPoster(filepath=compImgFile)
|
||||
|
||||
merge(first, episodes)
|
||||
|
||||
first.batchEdits() \
|
||||
.editTitle(title[:-3]) \
|
||||
.editSortTitle(titleSort[:-3]) \
|
||||
.editSummary(summary[:-2]) \
|
||||
.editContentRating(first.contentRating) \
|
||||
.editOriginallyAvailable(first.originallyAvailableAt) \
|
||||
.addWriter(writers) \
|
||||
.addDirector(directors) \
|
||||
|
||||
if renumber:
|
||||
first._edits['index.value'] = index
|
||||
first._edits['index.locked'] = 1
|
||||
|
||||
first.saveEdits()
|
||||
|
||||
|
||||
def merge(first, episodes):
|
||||
key = '%s/merge?ids=%s' % (first.key, ','.join([str(r.ratingKey) for r in episodes]))
|
||||
first._server.query(key, method=first._server._session.put)
|
||||
|
||||
|
||||
def download_image(url):
|
||||
r = requests.get(url, stream=True)
|
||||
r.raw.decode_content = True
|
||||
return r.raw
|
||||
|
||||
|
||||
def create_composite_thumb(firstImgFile, lastImgFile):
|
||||
mask, line = create_masks()
|
||||
|
||||
# Open and crop first image
|
||||
firstImg = Image.open(firstImgFile)
|
||||
width, height = firstImg.size
|
||||
firstImg = firstImg.crop(
|
||||
(
|
||||
(width - WIDTH) // 2,
|
||||
(height - HEIGHT) // 2,
|
||||
(width + WIDTH) // 2,
|
||||
(height + HEIGHT) // 2
|
||||
)
|
||||
)
|
||||
|
||||
# Open and crop last image
|
||||
lastImg = Image.open(lastImgFile)
|
||||
width, height = lastImg.size
|
||||
lastImg = lastImg.crop(
|
||||
(
|
||||
(width - WIDTH) // 2,
|
||||
(height - HEIGHT) // 2,
|
||||
(width + WIDTH) // 2,
|
||||
(height + HEIGHT) // 2
|
||||
)
|
||||
)
|
||||
|
||||
# Create composite image
|
||||
comp = Image.composite(line, Image.composite(firstImg, lastImg, mask), line)
|
||||
|
||||
# Return composite image as file-like object
|
||||
compImgFile = io.BytesIO()
|
||||
comp.save(compImgFile, format='jpeg')
|
||||
compImgFile.seek(0)
|
||||
return compImgFile
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def create_masks():
|
||||
scale = 3 # For line anti-aliasing
|
||||
offset = HEIGHT // 2 * math.tan(LINE_ANGLE * math.pi / 180)
|
||||
|
||||
# Create diagonal mask
|
||||
mask = Image.new('L', (WIDTH, HEIGHT), 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.polygon(
|
||||
(
|
||||
(0, 0),
|
||||
(WIDTH // 2 + offset, 0),
|
||||
(WIDTH // 2 - offset, HEIGHT),
|
||||
(0, HEIGHT)
|
||||
),
|
||||
fill=255
|
||||
)
|
||||
|
||||
# Create diagonal line (use larger image then scale down with anti-aliasing)
|
||||
line = Image.new('L', (scale * WIDTH, scale * HEIGHT), 0)
|
||||
draw = ImageDraw.Draw(line)
|
||||
draw.line(
|
||||
(
|
||||
(scale * (WIDTH // 2 + offset), -scale),
|
||||
(scale * (WIDTH // 2 - offset), scale * (HEIGHT + 1))
|
||||
),
|
||||
fill=255,
|
||||
width=scale * LINE_THICKNESS
|
||||
)
|
||||
line = line.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS)
|
||||
|
||||
return mask, line
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--library', required=True)
|
||||
parser.add_argument('--show', required=True)
|
||||
parser.add_argument('--renumber', action='store_true')
|
||||
parser.add_argument('--composite_thumb', action='store_true')
|
||||
opts = parser.parse_args()
|
||||
|
||||
if opts.composite_thumb and not hasPIL:
|
||||
print('PIL is not installed. Please install `pillow` to create composite thumbnails.')
|
||||
exit(1)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
group_episodes(plex, **vars(opts))
|
45
utility/music_folder_collections.py
Normal file
45
utility/music_folder_collections.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
audiobooks /
|
||||
-- book1 /
|
||||
-- book1 - chapter1.mp3 ...
|
||||
-- series1 /
|
||||
-- book1 /
|
||||
-- book1 - chapter1.mp3 ...
|
||||
-- book2 /
|
||||
-- book2 - chapter1.mp3 ...
|
||||
|
||||
In this structure use series1 to add all the series' books into a colleciton.
|
||||
|
||||
"""
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
COLLECTIONAME = 'My Fav Series'
|
||||
TOPLEVELFOLDERNAME = 'Series Name'
|
||||
LIBRARYNAME = 'Audio Books'
|
||||
|
||||
abLibrary = plex.library.section(LIBRARYNAME)
|
||||
|
||||
albums = []
|
||||
for folder in abLibrary.folders():
|
||||
if folder.title == TOPLEVELFOLDERNAME:
|
||||
for series in folder.allSubfolders():
|
||||
trackKey = series.key
|
||||
try:
|
||||
track = plex.fetchItem(trackKey)
|
||||
albumKey = track.parentKey
|
||||
album = plex.fetchItem(albumKey)
|
||||
albums.append(album)
|
||||
except Exception:
|
||||
# print('{} contains additional subfolders that were likely captured. \n[{}].'
|
||||
# .format(series.title, ', '.join([x.title for x in series.allSubfolders()])))
|
||||
pass
|
||||
|
||||
for album in list(set(albums)):
|
||||
print('Adding {} to collection {}.'.format(album.title, COLLECTIONAME))
|
||||
album.addCollection(COLLECTIONAME)
|
112
utility/off_deck.py
Normal file
112
utility/off_deck.py
Normal file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Removes Shows from Continue Watching.
|
||||
|
||||
Author: Blacktwin
|
||||
Requires: requests, plexapi
|
||||
|
||||
Example:
|
||||
python off_deck.py
|
||||
- Display what shows are on admin's Continue Watching
|
||||
|
||||
python off_deck.py --user Steve
|
||||
- Display what shows are on Steve's Continue Watching
|
||||
|
||||
python off_deck.py --shows "The Simpsons" Seinfeld
|
||||
- The Simpsons and Seinfeld Episodes will be removed from admin's Continue Watching
|
||||
|
||||
python off_deck.py --user Steve --shows "The Simpsons" Seinfeld
|
||||
- The Simpsons and Seinfeld Episodes will be removed from Steve's Continue Watching
|
||||
|
||||
python off_deck.py --playlists "Favorite Shows!"
|
||||
- Any Episode found in admin's "Favorite Shows" playlist will be remove from Continue Watching
|
||||
|
||||
python off_deck.py --user Steve --playlists "Favorite Shows!" SleepMix
|
||||
- Any Episode found in Steve's "Favorite Shows" or SleepMix playlist will be remove from Continue Watching
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import requests
|
||||
import argparse
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', '')
|
||||
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', '')
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
account = plex.myPlexAccount()
|
||||
|
||||
|
||||
def remove_from_cw(server, ratingKey):
|
||||
key = '/actions/removeFromContinueWatching?ratingKey=%s&' % ratingKey
|
||||
server.query(key, method=server._session.put)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Remove items from Continue Watching.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--shows', nargs='+',
|
||||
help='Shows to be removed from Continue Watching.')
|
||||
parser.add_argument('--user', nargs='?',
|
||||
help='User whose Continue Watching will be modified.')
|
||||
parser.add_argument('--playlists', nargs='+',
|
||||
help='Shows in playlist to be removed from Continue Watching')
|
||||
parser.add_argument('--markWatched', action='store_true',
|
||||
help='Mark episode as watched after removing from Continue Watching')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
to_remove = []
|
||||
|
||||
if opts.user:
|
||||
user_acct = account.user(opts.user)
|
||||
plex_server = PlexServer(PLEX_URL, user_acct.get_token(plex.machineIdentifier))
|
||||
else:
|
||||
plex_server = plex
|
||||
|
||||
onDeck = [item for item in plex_server.library.onDeck() if item.type == 'episode']
|
||||
|
||||
if opts.shows and not opts.playlists:
|
||||
for show in opts.shows:
|
||||
searched_show = plex_server.search(show, mediatype='show')[0]
|
||||
if searched_show.title == show:
|
||||
to_remove += searched_show.episodes()
|
||||
elif not opts.shows and opts.playlists:
|
||||
for pl in plex_server.playlists():
|
||||
if pl.title in opts.playlists:
|
||||
to_remove += pl.items()
|
||||
else:
|
||||
for item in onDeck:
|
||||
print('{}: S{:02}E{:02} {}'.format(item.grandparentTitle, int(item.parentIndex),
|
||||
int(item.index), item.title))
|
||||
|
||||
for item in onDeck:
|
||||
if item in to_remove:
|
||||
print('Removing {}: S{:02}E{:02} {} from Continue Watching'.format(
|
||||
item.grandparentTitle, int(item.parentIndex), int(item.index), item.title))
|
||||
# item.removeFromContinueWatching()
|
||||
remove_from_cw(plex_server, item.ratingKey)
|
||||
if opts.markWatched:
|
||||
print('Marking as watched!')
|
||||
item.markPlayed()
|
@ -1,14 +1,16 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Invite new users to share Plex libraries.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-s [], --share [] Share specific libraries or share all libraries.
|
||||
(choices: share, share_all)
|
||||
-u [], --user [] Enter a valid username(s) or email address(s) for user to be invited.
|
||||
-l [ ...], --libraries [ ...]
|
||||
--user [] Enter a valid username(s) or email address(s) for user to be invited.
|
||||
--libraries [ ...]
|
||||
Space separated list of case sensitive names to process. Allowed names are:
|
||||
(choices: All library names)
|
||||
--allLibraries Select all libraries.
|
||||
--sync Allow user to sync content
|
||||
--camera Allow user to upload photos
|
||||
--channel Allow user to utilize installed channels
|
||||
@ -20,30 +22,48 @@ optional arguments:
|
||||
|
||||
Usage:
|
||||
|
||||
plex_api_invite.py -s share -u USER -l Movies
|
||||
plex_api_invite.py --user USER --libraries Movies
|
||||
- Shared libraries: ['Movies'] with USER
|
||||
|
||||
plex_api_invite.py -s share -u USER -l Movies "TV Shows"
|
||||
plex_api_invite.py --user USER --libraries Movies "TV Shows"
|
||||
- Shared libraries: ['Movies', 'TV Shows'] with USER
|
||||
* Double Quote libraries with spaces
|
||||
|
||||
plex_api_invite.py -s share_all -u USER
|
||||
plex_api_invite.py --allLibraries --user USER
|
||||
- Shared all libraries with USER.
|
||||
|
||||
plex_api_invite.py -s share Movies -u USER --movieRatings G, PG-13
|
||||
plex_api_invite.py --libraries Movies --user USER --movieRatings G, PG-13
|
||||
- Share Movie library with USER but restrict them to only G and PG-13 titles.
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
import argparse
|
||||
import requests
|
||||
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxxx'
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', '')
|
||||
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', '')
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
sections_lst = [x.title for x in plex.library.sections()]
|
||||
@ -54,28 +74,37 @@ def invite(user, sections, allowSync, camera, channels, filterMovies, filterTele
|
||||
plex.myPlexAccount().inviteFriend(user=user, server=plex, sections=sections, allowSync=allowSync,
|
||||
allowCameraUpload=camera, allowChannels=channels, filterMovies=filterMovies,
|
||||
filterTelevision=filterTelevision, filterMusic=filterMusic)
|
||||
print('Invited {user} to share libraries: {libraries}.'.format(libraries=sections, user=user))
|
||||
print('Invited {user} to share libraries: \n{sections}'.format(sections=sections, user=user))
|
||||
if allowSync is True:
|
||||
print('Sync: Enabled')
|
||||
if camera is True:
|
||||
print('Camera Upload: Enabled')
|
||||
if channels is True:
|
||||
print('Plugins: Enabled')
|
||||
if filterMovies:
|
||||
print('Movie Filters: {}'.format(filterMovies))
|
||||
if filterTelevision:
|
||||
print('Show Filters: {}'.format(filterTelevision))
|
||||
if filterMusic:
|
||||
print('Music Filters: {}'.format(filterMusic))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description="Invite new users to share Plex libraries.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
|
||||
parser.add_argument('-s', '--share', nargs='?', type=str, required=True,
|
||||
choices=['share', 'share_all'], metavar='',
|
||||
help='Share specific libraries or share all libraries.: \n (choices: %(choices)s)')
|
||||
parser.add_argument('-u', '--user', nargs='+', type=str, required=True, metavar='',
|
||||
parser.add_argument('--user', nargs='+', required=True,
|
||||
help='Enter a valid username(s) or email address(s) for user to be invited.')
|
||||
parser.add_argument('-l', '--libraries', nargs='+', default='', choices=sections_lst, metavar='',
|
||||
parser.add_argument('--libraries', nargs='+', default=False, choices=sections_lst, metavar='',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s')
|
||||
|
||||
parser.add_argument('--sync', default=False, action='store_true',
|
||||
parser.add_argument('--allLibraries', default=False, action='store_true',
|
||||
help='Select all libraries.')
|
||||
parser.add_argument('--sync', default=None, action='store_true',
|
||||
help='Use to allow user to sync content.')
|
||||
parser.add_argument('--camera', default=False, action='store_true',
|
||||
parser.add_argument('--camera', default=None, action='store_true',
|
||||
help='Use to allow user to upload photos.')
|
||||
parser.add_argument('--channels', default=False, action='store_true',
|
||||
parser.add_argument('--channels', default=None, action='store_true',
|
||||
help='Use to allow user to utilize installed channels.')
|
||||
parser.add_argument('--movieRatings', nargs='+', choices=ratings_lst, metavar='',
|
||||
help='Use to add rating restrictions to movie library types.')
|
||||
@ -89,26 +118,51 @@ if __name__ == "__main__":
|
||||
help='Use to add label restrictions for music library types.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
libraries = ''
|
||||
|
||||
filterMovies = {}
|
||||
filterTelevision = {}
|
||||
filterMusic = {}
|
||||
# Plex Pass additional share options
|
||||
sync = None
|
||||
camera = None
|
||||
channels = None
|
||||
filterMovies = None
|
||||
filterTelevision = None
|
||||
filterMusic = None
|
||||
try:
|
||||
if opts.sync:
|
||||
sync = opts.sync
|
||||
if opts.camera:
|
||||
camera = opts.camera
|
||||
if opts.channels:
|
||||
channels = opts.channels
|
||||
if opts.movieLabels or opts.movieRatings:
|
||||
filterMovies = {}
|
||||
if opts.movieLabels:
|
||||
filterMovies['label'] = opts.movieLabels
|
||||
if opts.movieRatings:
|
||||
filterMovies['contentRating'] = opts.movieRatings
|
||||
if opts.tvLabels or opts.tvRatings:
|
||||
filterTelevision = {}
|
||||
if opts.tvLabels:
|
||||
filterTelevision['label'] = opts.tvLabels
|
||||
if opts.tvRatings:
|
||||
filterTelevision['contentRating'] = opts.tvRatings
|
||||
if opts.musicLabels:
|
||||
filterMusic = {}
|
||||
filterMusic['label'] = opts.musicLabels
|
||||
except AttributeError:
|
||||
print('No Plex Pass moving on...')
|
||||
|
||||
if opts.movieLabels:
|
||||
filterMovies['label'] = opts.movieLabels
|
||||
if opts.movieRatings:
|
||||
filterMovies['contentRating'] = opts.movieRatings
|
||||
if opts.tvLabels:
|
||||
filterTelevision['label'] = opts.tvLabels
|
||||
if opts.tvRatings:
|
||||
filterTelevision['contentRating'] = opts.tvRatings
|
||||
if opts.musicLabels:
|
||||
filterMusic['label'] = opts.musicLabels
|
||||
# Defining libraries
|
||||
if opts.allLibraries and not opts.libraries:
|
||||
libraries = sections_lst
|
||||
elif not opts.allLibraries and opts.libraries:
|
||||
libraries = opts.libraries
|
||||
elif opts.allLibraries and opts.libraries:
|
||||
# If allLibraries is used then any libraries listed will be excluded
|
||||
for library in opts.libraries:
|
||||
sections_lst.remove(library)
|
||||
libraries = sections_lst
|
||||
|
||||
for user in opts.user:
|
||||
if opts.share == 'share':
|
||||
invite(user, opts.libraries, opts.sync, opts.camera, opts.channels,
|
||||
filterMovies, filterTelevision, filterMusic)
|
||||
elif opts.share == 'share_all':
|
||||
invite(user, sections_lst, opts.sync, opts.camera, opts.channels,
|
||||
filterMovies, filterTelevision, filterMusic)
|
||||
invite(user, libraries, sync, camera, channels,
|
||||
filterMovies, filterTelevision, filterMusic)
|
||||
|
@ -1,4 +1,7 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Set as cron or task for times of allowing and not allowing user access to server.
|
||||
Unsharing will kill any current stream from user before unsharing.
|
||||
|
||||
@ -32,23 +35,40 @@ Usage:
|
||||
- Unshared all libraries with USER.
|
||||
- USER is still exists as a Friend or Home User
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
from time import sleep
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
|
||||
MESSAGE = "GET TO BED!"
|
||||
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxxx'
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
user_lst = [x.title for x in plex.myPlexAccount().users()]
|
||||
sections_lst = [x.title for x in plex.library.sections()]
|
||||
|
||||
MESSAGE = "GET TO BED!"
|
||||
|
||||
|
||||
def share(user, libraries):
|
||||
plex.myPlexAccount().updateFriend(user=user, server=plex, sections=libraries)
|
||||
|
@ -1,5 +1,11 @@
|
||||
"""
|
||||
Description: Pull Movie and TV Show poster images from Plex. Save to Movie and TV Show directories in scripts working directory.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Pull Movie and TV Show poster images from Plex.
|
||||
|
||||
Saves the poster images to Movie and TV Show directories in scripts working
|
||||
directory.
|
||||
|
||||
Author: Blacktwin
|
||||
Requires: plexapi
|
||||
|
||||
@ -7,19 +13,41 @@ Requires: plexapi
|
||||
python plex_api_poster_pull.py
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
library_name = ['Movies', 'TV Shows'] # Your library names
|
||||
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', '')
|
||||
|
||||
library_name = ['Movies','TV Shows'] # You library names
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', '')
|
||||
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
# Create paths for Movies and TV Shows inside current directory
|
||||
movie_path = '{}/Movies'.format(os.path.dirname(__file__))
|
||||
@ -35,7 +63,7 @@ if not os.path.isdir(show_path):
|
||||
for library in library_name:
|
||||
for child in plex.library.section(library).all():
|
||||
# Clean names of special characters
|
||||
name = re.sub('\W+',' ', child.title)
|
||||
name = re.sub('\W+', ' ', child.title)
|
||||
# Add (year) to name
|
||||
name = '{} ({})'.format(name, child.year)
|
||||
# Pull URL for poster
|
||||
@ -50,4 +78,4 @@ for library in library_name:
|
||||
print("ERROR, %s already exist" % image_path)
|
||||
else:
|
||||
# Save to directory
|
||||
urllib.urlretrieve(thumb_url, image_path)
|
||||
urllib.request.urlretrieve(thumb_url, image_path)
|
||||
|
@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
'''
|
||||
Share or unshare libraries.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Share or unshare libraries.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--share To share libraries.
|
||||
-h, --help Show this help message and exit
|
||||
--share To share libraries to user.
|
||||
--shared Display user's share settings.
|
||||
--unshare To unshare all libraries.
|
||||
--kill Kill user's current stream(s). Include message to override default message
|
||||
--add Add additional libraries.
|
||||
--remove Remove existing libraries.
|
||||
--unshare To unshare all libraries from user.
|
||||
--add Share additional libraries or enable settings to user.
|
||||
--remove Remove shared libraries or disable settings from user.
|
||||
--user [ ...] Space separated list of case sensitive names to process. Allowed names are:
|
||||
(choices: All users names)
|
||||
--allUsers Select all users.
|
||||
@ -18,6 +18,14 @@ optional arguments:
|
||||
(choices: All library names)
|
||||
(default: All Libraries)
|
||||
--allLibraries Select all libraries.
|
||||
--backup Backup share settings from json file
|
||||
--restore Restore share settings from json file
|
||||
Filename of json file to use.
|
||||
(choices: %(json files found in cwd)s)
|
||||
--libraryShares Show all shares by library
|
||||
|
||||
# Plex Pass member only settings:
|
||||
--kill Kill user's current stream(s). Include message to override default message
|
||||
--sync Allow user to sync content
|
||||
--camera Allow user to upload photos
|
||||
--channel Allow user to utilize installed channels
|
||||
@ -26,10 +34,6 @@ optional arguments:
|
||||
--tvRatings Add rating restrictions to show library types
|
||||
--tvLabels Add label restrictions to show library types
|
||||
--musicLabels Add label restrictions to music library types
|
||||
--backup Backup share settings from json file
|
||||
--restore Restore share settings from json file
|
||||
Filename of json file to use.
|
||||
(choices: %(json files found in cwd)s)
|
||||
|
||||
Usage:
|
||||
|
||||
@ -63,12 +67,24 @@ Usage:
|
||||
plex_api_share.py --backup
|
||||
- Backup all user shares to a json file
|
||||
|
||||
plex_api_share.py --backup --user USER
|
||||
- Backup USER shares to a json file
|
||||
|
||||
plex_api_share.py --restore
|
||||
- Only restore all Plex user's shares and settings from backup json file
|
||||
|
||||
plex_api_share.py --restore --user USER
|
||||
- Only restore USER's Plex shares and settings from backup json file
|
||||
|
||||
plex_api_share.py --user USER --add --sync
|
||||
- Enable sync feature for USER
|
||||
|
||||
plex_api_share.py --user USER --remove --sync
|
||||
- Disable sync feature for USER
|
||||
|
||||
plex_api_share.py --libraryShares
|
||||
- {Library Name} is shared to the following users:
|
||||
{USERS}
|
||||
|
||||
Excluding;
|
||||
|
||||
@ -81,7 +97,9 @@ Usage:
|
||||
plex_api_share.py --share -u USER --allLibraries --libraries Movies
|
||||
- Shared [all libraries but Movies] with USER.
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
import time
|
||||
@ -92,13 +110,14 @@ import json
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
|
||||
DEFAULT_MESSAGE = "Steam is being killed by admin."
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', '')
|
||||
|
||||
json_check = sorted([f for f in os.listdir('.') if os.path.isfile(f) and
|
||||
f.endswith(".json")], key=os.path.getmtime)
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', '')
|
||||
|
||||
DEFAULT_MESSAGE = "Stream is being killed by admin."
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
@ -114,38 +133,51 @@ if sess.verify is False:
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
user_lst = [x.title for x in plex.myPlexAccount().users()]
|
||||
user_lst = {x.title: x.email if x.email else x.title for x in plex.myPlexAccount().users() if x.title}
|
||||
user_choices = list(set(user_lst.values())) + list(user_lst.keys())
|
||||
sections_lst = [x.title for x in plex.library.sections()]
|
||||
movies_keys = [x.key for x in plex.library.sections() if x.type == 'movie']
|
||||
show_keys = [x.key for x in plex.library.sections() if x.type == 'show']
|
||||
|
||||
json_check = sorted([f for f in os.listdir('.') if os.path.isfile(f) and
|
||||
f.endswith(".json") and
|
||||
f.startswith(plex.friendlyName)],
|
||||
key=os.path.getmtime)
|
||||
|
||||
my_server_names = []
|
||||
# Find all owners server names. For owners with multiple servers.
|
||||
for res in plex.myPlexAccount().resources():
|
||||
if res.provides == 'server' and res.owned == True:
|
||||
if res.provides == 'server' and res.owned is True:
|
||||
my_server_names.append(res.name)
|
||||
|
||||
ALLOWED_MEDIA_FILTERS = ('contentRating', 'contentRating!', 'label', 'label!')
|
||||
|
||||
|
||||
def get_ratings_lst(section_id):
|
||||
headers = {'Accept': 'application/json'}
|
||||
params = {'X-Plex-Token': PLEX_TOKEN}
|
||||
content = requests.get("{}/library/sections/{}/contentRating".format(PLEX_URL, section_id),
|
||||
headers=headers, params=params)
|
||||
content = sess.get("{}/library/sections/{}/contentRating".format(PLEX_URL, section_id),
|
||||
headers=headers, params=params)
|
||||
|
||||
ratings_keys = content.json()['MediaContainer']['Directory']
|
||||
ratings_lst = [x['title'] for x in ratings_keys]
|
||||
return ratings_lst
|
||||
try:
|
||||
ratings_keys = content.json()['MediaContainer']['Directory']
|
||||
ratings_lst = [x['title'] for x in ratings_keys]
|
||||
return ratings_lst
|
||||
except Exception:
|
||||
print("Unable to pull ratings from section ID: {}.".format(section_id))
|
||||
pass
|
||||
|
||||
|
||||
def filter_clean(filter_type):
|
||||
clean = ''
|
||||
try:
|
||||
clean = dict(item.split("=") for item in filter_type.split("|"))
|
||||
filter_type = filter_type.replace('|', '&')
|
||||
clean = dict(item.split("=") for item in filter_type.split("&"))
|
||||
for k, v in clean.items():
|
||||
labels = v.replace('%20', ' ')
|
||||
labels = labels.split('%2C')
|
||||
clean[k] = labels
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
return clean
|
||||
|
||||
@ -165,17 +197,16 @@ def find_shares(user):
|
||||
'filterMovies': filter_clean(user_acct.filterMovies),
|
||||
'filterTelevision': filter_clean(user_acct.filterTelevision),
|
||||
'filterMusic': filter_clean(user_acct.filterMusic),
|
||||
'servers': []}
|
||||
'serverName': plex.friendlyName,
|
||||
'sections': ""}
|
||||
|
||||
for server in user_acct.servers:
|
||||
if server.name in my_server_names:
|
||||
if server.name == plex.friendlyName:
|
||||
sections = []
|
||||
for section in server.sections():
|
||||
if section.shared == True:
|
||||
if section.shared is True:
|
||||
sections.append(section.title)
|
||||
user_backup['servers'].append({'serverName': server.name,
|
||||
'sections': sections,
|
||||
'sectionCount': len(sections)})
|
||||
user_backup['sections'] = sections
|
||||
|
||||
return user_backup
|
||||
|
||||
@ -197,25 +228,57 @@ def share(user, sections, allowSync, camera, channels, filterMovies, filterTelev
|
||||
plex.myPlexAccount().updateFriend(user=user, server=plex, sections=sections, allowSync=allowSync,
|
||||
allowCameraUpload=camera, allowChannels=channels, filterMovies=filterMovies,
|
||||
filterTelevision=filterTelevision, filterMusic=filterMusic)
|
||||
print('Shared libraries: {sections} with {user}.'.format(sections=sections, user=user))
|
||||
print('Settings: Sync: {}, Camer Upload: {}, Channels: {}, Movie Filters: {}, TV Filters: {}, Music Filter: {}'.
|
||||
format(allowSync, camera, channels, filterMovies, filterTelevision, filterMusic))
|
||||
if sections:
|
||||
print('{user}\'s updated shared libraries: \n{sections}'.format(sections=sections, user=user))
|
||||
if allowSync is True:
|
||||
print('Sync: Enabled')
|
||||
if allowSync is False:
|
||||
print('Sync: Disabled')
|
||||
if camera is True:
|
||||
print('Camera Upload: Enabled')
|
||||
if camera is False:
|
||||
print('Camera Upload: Disabled')
|
||||
if channels is True:
|
||||
print('Plugins: Enabled')
|
||||
if channels is False:
|
||||
print('Plugins: Disabled')
|
||||
if filterMovies:
|
||||
print('Movie Filters: {}'.format(filterMovies))
|
||||
if filterMovies == {}:
|
||||
print('Movie Filters:')
|
||||
if filterTelevision:
|
||||
print('Show Filters: {}'.format(filterTelevision))
|
||||
if filterTelevision == {}:
|
||||
print('Show Filters:')
|
||||
if filterMusic:
|
||||
print('Music Filters: {}'.format(filterMusic))
|
||||
if filterMusic == {} and filterMusic is not None:
|
||||
print('Music Filters:')
|
||||
|
||||
|
||||
def unshare(user, sections):
|
||||
plex.myPlexAccount().updateFriend(user=user, server=plex, removeSections=True, sections=sections)
|
||||
print('Unuser_shares all libraries from {user}.'.format(user=user))
|
||||
print('Unshared all libraries from {user}.'.format(user=user))
|
||||
|
||||
|
||||
def add_to_dictlist(d, key, val):
|
||||
if key not in d:
|
||||
d[key] = [val]
|
||||
else:
|
||||
d[key].append(val)
|
||||
|
||||
|
||||
def allowed_filters(filters, filterDict):
|
||||
for filter in filters[0]:
|
||||
if filter[0] in ALLOWED_MEDIA_FILTERS:
|
||||
add_to_dictlist(filterDict, filter[0], filter[1])
|
||||
else:
|
||||
print("{} is not among the allowed keys for this argument.\n"
|
||||
"Allowed keys: {}".format(filter[0], ','.join(ALLOWED_MEDIA_FILTERS)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
movie_ratings = []
|
||||
show_ratings = []
|
||||
for movie in movies_keys:
|
||||
movie_ratings += get_ratings_lst(movie)
|
||||
for show in show_keys:
|
||||
show_ratings += get_ratings_lst(show)
|
||||
|
||||
timestr = time.strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Share or unshare libraries.",
|
||||
@ -226,13 +289,11 @@ if __name__ == "__main__":
|
||||
help='Display user\'s shared libraries.')
|
||||
parser.add_argument('--unshare', default=False, action='store_true',
|
||||
help='To unshare all libraries.')
|
||||
parser.add_argument('--kill', default=False, nargs='?',
|
||||
help='Kill user\'s current stream(s). Include message to override default message.')
|
||||
parser.add_argument('--add', default=False, action='store_true',
|
||||
help='Add additional libraries.')
|
||||
help='Share additional libraries or enable settings to user..')
|
||||
parser.add_argument('--remove', default=False, action='store_true',
|
||||
help='Remove existing libraries.')
|
||||
parser.add_argument('--user', nargs='+', choices=user_lst, metavar='',
|
||||
help='Remove shared libraries or disable settings from user.')
|
||||
parser.add_argument('--user', nargs='+', choices=user_choices, metavar='',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s)')
|
||||
parser.add_argument('--allUsers', default=False, action='store_true',
|
||||
@ -242,63 +303,105 @@ if __name__ == "__main__":
|
||||
'(choices: %(choices)s')
|
||||
parser.add_argument('--allLibraries', default=False, action='store_true',
|
||||
help='Select all libraries.')
|
||||
parser.add_argument('--sync', default=False, action='store_true',
|
||||
help='Use to allow user to sync content.')
|
||||
parser.add_argument('--camera', default=False, action='store_true',
|
||||
help='Use to allow user to upload photos.')
|
||||
parser.add_argument('--channels', default=False, action='store_true',
|
||||
help='Use to allow user to utilize installed channels.')
|
||||
parser.add_argument('--movieRatings', nargs='+', choices=list(set(movie_ratings)), metavar='',
|
||||
help='Use to add rating restrictions to movie library types.\n'
|
||||
'Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s')
|
||||
parser.add_argument('--movieLabels', nargs='+', metavar='',
|
||||
help='Use to add label restrictions for movie library types.')
|
||||
parser.add_argument('--tvRatings', nargs='+', choices=list(set(show_ratings)), metavar='',
|
||||
help='Use to add rating restrictions for show library types.\n'
|
||||
'Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s')
|
||||
parser.add_argument('--tvLabels', nargs='+', metavar='',
|
||||
help='Use to add label restrictions for show library types.')
|
||||
parser.add_argument('--musicLabels', nargs='+', metavar='',
|
||||
help='Use to add label restrictions for music library types.')
|
||||
|
||||
parser.add_argument('--backup', default=False, action='store_true',
|
||||
help='Backup share settings from json file.')
|
||||
parser.add_argument('--restore', type = str, choices = json_check,
|
||||
parser.add_argument('--restore', type=str, choices=json_check, metavar='',
|
||||
help='Restore share settings from json file.\n'
|
||||
'Filename of json file to use.\n'
|
||||
'(choices: %(choices)s)')
|
||||
parser.add_argument('--libraryShares', default=False, action='store_true',
|
||||
help='Show all shares by library.')
|
||||
|
||||
# For Plex Pass members
|
||||
if plex.myPlexSubscription is True:
|
||||
movie_ratings = []
|
||||
show_ratings = []
|
||||
for movie in movies_keys:
|
||||
ratings = get_ratings_lst(movie)
|
||||
if ratings: movie_ratings += ratings
|
||||
for show in show_keys:
|
||||
ratings = get_ratings_lst(show)
|
||||
if ratings: show_ratings += ratings
|
||||
parser.add_argument('--kill', default=None, nargs='?',
|
||||
help='Kill user\'s current stream(s). Include message to override default message.')
|
||||
parser.add_argument('--sync', default=None, action='store_true',
|
||||
help='Use to allow user to sync content.')
|
||||
parser.add_argument('--camera', default=None, action='store_true',
|
||||
help='Use to allow user to upload photos.')
|
||||
parser.add_argument('--channels', default=None, action='store_true',
|
||||
help='Use to allow user to utilize installed channels.')
|
||||
parser.add_argument('--movieRatings', nargs='+', choices=list(set(movie_ratings)), metavar='',
|
||||
help='Use to add rating restrictions to movie library types.\n'
|
||||
'Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s')
|
||||
parser.add_argument('--movieLabels', nargs='+', action='append', type=lambda kv: kv.split("="),
|
||||
help='Use to add label restrictions for movie library types.')
|
||||
parser.add_argument('--tvRatings', nargs='+', choices=list(set(show_ratings)), metavar='',
|
||||
help='Use to add rating restrictions for show library types.\n'
|
||||
'Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s')
|
||||
parser.add_argument('--tvLabels', nargs='+', action='append', type=lambda kv: kv.split("="),
|
||||
help='Use to add label restrictions for show library types.')
|
||||
parser.add_argument('--musicLabels', nargs='+', metavar='',
|
||||
help='Use to add label restrictions for music library types.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
users = ''
|
||||
libraries = ''
|
||||
filterMovies = {}
|
||||
filterTelevision = {}
|
||||
filterMusic = {}
|
||||
|
||||
# Setting additional share options
|
||||
if opts.movieLabels:
|
||||
filterMovies['label'] = opts.movieLabels
|
||||
if opts.movieRatings:
|
||||
filterMovies['contentRating'] = opts.movieRatings
|
||||
if opts.tvLabels:
|
||||
filterTelevision['label'] = opts.tvLabels
|
||||
if opts.tvRatings:
|
||||
filterTelevision['contentRating'] = opts.tvRatings
|
||||
if opts.musicLabels:
|
||||
filterMusic['label'] = opts.musicLabels
|
||||
# Plex Pass additional share options
|
||||
kill = None
|
||||
sync = None
|
||||
camera = None
|
||||
channels = None
|
||||
filterMovies = None
|
||||
filterTelevision = None
|
||||
filterMusic = None
|
||||
try:
|
||||
if opts.kill:
|
||||
kill = opts.kill
|
||||
if opts.sync:
|
||||
sync = opts.sync
|
||||
if opts.camera:
|
||||
camera = opts.camera
|
||||
if opts.channels:
|
||||
channels = opts.channels
|
||||
if opts.movieLabels or opts.movieRatings:
|
||||
filterMovies = {}
|
||||
if opts.movieLabels:
|
||||
allowed_filters(opts.movieLabels, filterMovies)
|
||||
if opts.movieRatings:
|
||||
allowed_filters(opts.movieRatings, filterMovies)
|
||||
if opts.tvLabels or opts.tvRatings:
|
||||
filterTelevision = {}
|
||||
if opts.tvLabels:
|
||||
allowed_filters(opts.tvLabels, filterTelevision)
|
||||
if opts.tvRatings:
|
||||
allowed_filters(opts.tvRatings, filterTelevision)
|
||||
if opts.musicLabels:
|
||||
filterMusic = {}
|
||||
allowed_filters(opts.musicLabels, filterMusic)
|
||||
except AttributeError:
|
||||
print('No Plex Pass moving on...')
|
||||
|
||||
# Defining users
|
||||
if opts.allUsers and not opts.user:
|
||||
users = user_lst
|
||||
users = user_lst.keys()
|
||||
elif not opts.allUsers and opts.user:
|
||||
users = opts.user
|
||||
elif opts.allUsers and opts.user:
|
||||
# If allUsers is used then any users listed will be excluded
|
||||
for user in opts.user:
|
||||
user_lst.remove(user)
|
||||
users = user_lst
|
||||
# If username is used then remove
|
||||
if user_lst.get(user):
|
||||
del user_lst[user]
|
||||
# Else email is used and must find it's corresponding username and remove
|
||||
else:
|
||||
for k, v in user_lst.items():
|
||||
if v == user:
|
||||
del user_lst[k]
|
||||
|
||||
users = user_lst.keys()
|
||||
|
||||
# Defining libraries
|
||||
if opts.allLibraries and not opts.libraries:
|
||||
@ -311,41 +414,89 @@ if __name__ == "__main__":
|
||||
sections_lst.remove(library)
|
||||
libraries = sections_lst
|
||||
|
||||
if opts.libraryShares:
|
||||
users = user_lst.keys()
|
||||
user_sections = {}
|
||||
for user in users:
|
||||
user_shares_lst = find_shares(user)
|
||||
user_sections[user] = user_shares_lst['sections']
|
||||
|
||||
section_users = {}
|
||||
for user, sections in user_sections.items():
|
||||
for section in sections:
|
||||
section_users.setdefault(section, []).append(user)
|
||||
|
||||
for section, users in section_users.items():
|
||||
print("{} is shared to the following users:\n {}\n".format(section, ", ".join(users)))
|
||||
exit()
|
||||
|
||||
# Share, Unshare, Kill, Add, or Remove
|
||||
for user in users:
|
||||
user_shares = find_shares(user)
|
||||
user_shares_lst = user_shares['sections']
|
||||
if libraries:
|
||||
if opts.share:
|
||||
share(user, libraries, opts.sync, opts.camera, opts.channels, filterMovies, filterTelevision,
|
||||
share(user, libraries, sync, camera, channels, filterMovies, filterTelevision,
|
||||
filterMusic)
|
||||
if opts.add and user_shares['sections']:
|
||||
libraries = libraries + user_shares['sections']
|
||||
libraries = list(set(libraries))
|
||||
share(user, libraries, opts.sync, opts.camera, opts.channels, filterMovies, filterTelevision,
|
||||
if opts.add and user_shares_lst:
|
||||
addedLibraries = libraries + user_shares_lst
|
||||
addedLibraries = list(set(addedLibraries))
|
||||
share(user, addedLibraries, sync, camera, channels, filterMovies, filterTelevision,
|
||||
filterMusic)
|
||||
if opts.remove and user_shares['sections']:
|
||||
libraries = [sect for sect in user_shares['sections'] if sect not in libraries]
|
||||
share(user, libraries, opts.sync, opts.camera, opts.channels, filterMovies, filterTelevision,
|
||||
if opts.remove and user_shares_lst:
|
||||
removedLibraries = [sect for sect in user_shares_lst if sect not in libraries]
|
||||
share(user, removedLibraries, sync, camera, channels, filterMovies, filterTelevision,
|
||||
filterMusic)
|
||||
else:
|
||||
if opts.add:
|
||||
# Add/Enable settings independently of libraries
|
||||
addedLibraries = user_shares_lst
|
||||
share(user, addedLibraries, sync, camera, channels, filterMovies, filterTelevision,
|
||||
filterMusic)
|
||||
if opts.remove:
|
||||
# Remove/Disable settings independently of libraries
|
||||
# If remove and setting arg is True then flip setting to false to disable
|
||||
if sync:
|
||||
sync = False
|
||||
if camera:
|
||||
camera = False
|
||||
if channels:
|
||||
channels = False
|
||||
# Filters are cleared
|
||||
# todo-me clear completely or pop arg values?
|
||||
if filterMovies:
|
||||
filterMovies = {}
|
||||
if filterTelevision:
|
||||
filterTelevision = {}
|
||||
if filterMusic:
|
||||
filterMusic = {}
|
||||
share(user, libraries, sync, camera, channels, filterMovies, filterTelevision,
|
||||
filterMusic)
|
||||
|
||||
if opts.shared:
|
||||
user_json = json.dumps(user_shares, indent=4, sort_keys=True)
|
||||
print('Current share settings for {}: {}'.format(user, user_json))
|
||||
if opts.unshare and opts.kill:
|
||||
kill_session(user, opts.kill)
|
||||
if opts.unshare and kill:
|
||||
kill_session(user, kill)
|
||||
time.sleep(3)
|
||||
unshare(user, sections_lst)
|
||||
elif opts.unshare:
|
||||
elif opts.unshare and user_shares_lst:
|
||||
unshare(user, sections_lst)
|
||||
elif opts.kill:
|
||||
kill_session(user, opts.kill)
|
||||
elif opts.unshare and not user_shares_lst:
|
||||
print('{} has no libraries shared...'.format(user))
|
||||
elif kill:
|
||||
kill_session(user, kill)
|
||||
|
||||
if opts.backup:
|
||||
print('Backing up share information...')
|
||||
users_shares = []
|
||||
for user in user_lst:
|
||||
# If user arg is defined then abide, else backup all
|
||||
if not users:
|
||||
users = user_lst
|
||||
for user in users:
|
||||
# print('...Found {}'.format(user))
|
||||
users_shares.append(find_shares(user))
|
||||
json_file = 'Plex_share_backup_{}.json'.format(timestr)
|
||||
json_file = '{}_Plex_share_backup_{}.json'.format(plex.friendlyName, timestr)
|
||||
with open(json_file, 'w') as fp:
|
||||
json.dump(users_shares, fp, indent=4, sort_keys=True)
|
||||
|
||||
@ -354,16 +505,15 @@ if __name__ == "__main__":
|
||||
with open(''.join(opts.restore)) as json_data:
|
||||
shares_file = json.load(json_data)
|
||||
for user in shares_file:
|
||||
for server in user['servers']:
|
||||
# If user arg is defined then abide, else restore all
|
||||
if users:
|
||||
if user['title'] in users:
|
||||
print('Restoring user {}\'s shares and settings...'.format(user['title']))
|
||||
share(user['title'], server['sections'], user['allowSync'], user['camera'],
|
||||
user['channels'], user['filterMovies'], user['filterTelevision'],
|
||||
user['filterMusic'])
|
||||
else:
|
||||
# If user arg is defined then abide, else restore all
|
||||
if users:
|
||||
if user['title'] in users:
|
||||
print('Restoring user {}\'s shares and settings...'.format(user['title']))
|
||||
share(user['title'], server['sections'], user['allowSync'], user['camera'],
|
||||
share(user['title'], user['sections'], user['allowSync'], user['camera'],
|
||||
user['channels'], user['filterMovies'], user['filterTelevision'],
|
||||
user['filterMusic'])
|
||||
user['filterMusic'])
|
||||
else:
|
||||
print('Restoring user {}\'s shares and settings...'.format(user['title']))
|
||||
share(user['title'], user['sections'], user['allowSync'], user['camera'],
|
||||
user['channels'], user['filterMovies'], user['filterTelevision'],
|
||||
user['filterMusic'])
|
||||
|
@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Change show deletion settings by library.
|
||||
|
||||
@ -21,34 +24,47 @@ python plex_api_show_settings.py --libraries "TV Shows" --watched 7
|
||||
python plex_api_show_settings.py --libraries "TV Shows" --unwatched -7
|
||||
- Keep Episodesfrom the past 7 days
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
import argparse
|
||||
import requests
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
|
||||
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxx'
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
|
||||
# Allowed days/episodes to keep or delete
|
||||
WATCHED_LST = [0, 1, 7]
|
||||
UNWATCHED_LST = [0, 5, 3, 1, -3, -7,-30]
|
||||
UNWATCHED_LST = [0, 5, 3, 1, -3, -7, -30]
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
sections_lst = [x.title for x in plex.library.sections() if x.type == 'show']
|
||||
|
||||
|
||||
def set(rating_key, action, number):
|
||||
def set_show(rating_key, action, number):
|
||||
|
||||
path = '{}/prefs'.format(rating_key)
|
||||
try:
|
||||
params = {'X-Plex-Token': PLEX_TOKEN,
|
||||
action: number
|
||||
}
|
||||
action: number
|
||||
}
|
||||
|
||||
r = requests.put(PLEX_URL + path, params=params, verify=False)
|
||||
r = requests.put(PLEX_URL + path, params=params, verify=False)
|
||||
print(r.url)
|
||||
except Exception as e:
|
||||
print('Error: {}'.format(e))
|
||||
@ -82,4 +98,4 @@ if __name__ == '__main__':
|
||||
shows = plex.library.section(libary).all()
|
||||
|
||||
for show in shows:
|
||||
set(show.key, setting, number)
|
||||
set_show(show.key, setting, number)
|
||||
|
173
utility/plex_dance.py
Normal file
173
utility/plex_dance.py
Normal file
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Description: Do the Plex Dance!
|
||||
Author: Blacktwin, SwiftPanda16
|
||||
Requires: plexapi, requests
|
||||
|
||||
Original Dance moves
|
||||
1. Move all files for the media item out of the directory your Library is looking at,
|
||||
so Plex will not “see” it anymore
|
||||
2. Scan the library (to detect changes)
|
||||
3. Empty trash
|
||||
4. Clean bundles
|
||||
5. Double check naming schema and move files back
|
||||
6. Scan the library
|
||||
|
||||
https://forums.plex.tv/t/the-plex-dance/197064
|
||||
|
||||
Script Dance moves
|
||||
1. Create .plexignore file in affected items library root
|
||||
.plexignore will contain:
|
||||
|
||||
# Ignoring below file for Plex Dance
|
||||
*Item_Root/*
|
||||
|
||||
- if .plexignore file already exists in library root, append contents
|
||||
2. Scan the library
|
||||
3. Empty trash
|
||||
4. Clean bundles
|
||||
5. Remove or restore .plexignore
|
||||
6. Scan the library
|
||||
7. Optimize DB
|
||||
|
||||
Example:
|
||||
Dance with rating key 110645
|
||||
plex_dance.py --ratingKey 110645
|
||||
|
||||
From Unraid host OS
|
||||
plex_dance.py --ratingKey 110645 --path /mnt/user
|
||||
|
||||
*Dancing only works with Show or Movie rating keys
|
||||
|
||||
**After Dancing, if you use Tautulli the rating key of the dancing item will have changed.
|
||||
Please use this script to update your Tautulli database with the new rating key
|
||||
https://gist.github.com/JonnyWong16/f554f407832076919dc6864a78432db2
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import CONFIG
|
||||
import requests
|
||||
import argparse
|
||||
import time
|
||||
import os
|
||||
|
||||
# Using CONFIG file
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
IGNORE_FILE = "# Ignoring below file for Plex Dance\n{}"
|
||||
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
|
||||
if not PLEX_URL:
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl')
|
||||
|
||||
session = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
session.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if session.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=session)
|
||||
|
||||
|
||||
def section_path(section, filepath):
|
||||
for location in section.locations:
|
||||
if filepath.startswith(location):
|
||||
return location
|
||||
|
||||
|
||||
def refresh_section(sectionID):
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=session)
|
||||
section = plex.library.sectionByID(sectionID)
|
||||
section.update()
|
||||
time.sleep(10)
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=session)
|
||||
section = plex.library.sectionByID(sectionID)
|
||||
while section.refreshing is True:
|
||||
time.sleep(10)
|
||||
print("Waiting for library to finish refreshing to continue dance.")
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=session)
|
||||
section = plex.library.sectionByID(sectionID)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Do the Plex Dance!",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--ratingKey', nargs="?", type=int, required=True,
|
||||
help='Rating key of item that needs to dance.')
|
||||
parser.add_argument('--path', nargs="?", type=str,
|
||||
help='Prefix path for libraries behind mount points.\n'
|
||||
'Example: /mnt/user Resolves: /mnt/user/library_root')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
item = plex.fetchItem(opts.ratingKey)
|
||||
item.reload()
|
||||
sectionID = item.librarySectionID
|
||||
section = plex.library.sectionByID(sectionID)
|
||||
old_plexignore = ''
|
||||
|
||||
if item.type == 'movie':
|
||||
item_file = item.media[0].parts[0].file
|
||||
locations = os.path.split(item.locations[0])
|
||||
item_root = os.path.split(locations[0])[1]
|
||||
library_root = section_path(section, locations[0])
|
||||
elif item.type == 'show':
|
||||
locations = os.path.split(item.locations[0])
|
||||
item_root = locations[1]
|
||||
library_root = section_path(section, locations[0])
|
||||
else:
|
||||
print("Media type not supported.")
|
||||
exit()
|
||||
|
||||
library_root = opts.path + library_root if opts.path else library_root
|
||||
plexignore = IGNORE_FILE.format('*' + item_root + '/*')
|
||||
item_ignore = os.path.join(library_root, '.plexignore')
|
||||
# Check for existing .plexignore file in library root
|
||||
if os.path.exists(item_ignore):
|
||||
# If file exists append new ignore params and store old params
|
||||
with open(item_ignore, 'a+') as old_file:
|
||||
old_plexignore = old_file.readlines()
|
||||
old_file.write('\n' + plexignore)
|
||||
|
||||
# 1. Create .plexignore file
|
||||
print("Creating .plexignore file for dancing.")
|
||||
with open(item_ignore, 'w') as f:
|
||||
f.write(plexignore)
|
||||
# 2. Scan library
|
||||
print("Refreshing library of dancing item.")
|
||||
refresh_section(sectionID)
|
||||
# 3. Empty library trash
|
||||
print("Emptying Trash from library.")
|
||||
section.emptyTrash()
|
||||
time.sleep(5)
|
||||
# 4. Clean library bundles
|
||||
print("Cleaning Bundles from library.")
|
||||
plex.library.cleanBundles()
|
||||
time.sleep(5)
|
||||
# 5. Remove or restore .plexignore
|
||||
if old_plexignore:
|
||||
print("Replacing new .plexignore with old .plexignore.")
|
||||
with open(item_ignore, 'w') as new_file:
|
||||
new_file.writelines(old_plexignore)
|
||||
else:
|
||||
print("Removing .plexignore file from dancing directory.")
|
||||
os.remove(item_ignore)
|
||||
# 6. Scan library
|
||||
print("Refreshing library of dancing item.")
|
||||
refresh_section(sectionID)
|
||||
# 7. Optimize DB
|
||||
print("Optimizing library database.")
|
||||
plex.library.optimize()
|
@ -1,26 +1,35 @@
|
||||
'''
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Pull poster images from Imgur and places them inside Shows root folder.
|
||||
/path/to/show/Show.jpg
|
||||
|
||||
|
||||
Skips download if showname.jpg exists or if show does not exist.
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
from builtins import object
|
||||
import requests
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import os
|
||||
|
||||
|
||||
## Edit ##
|
||||
# ## Edit ##
|
||||
|
||||
# Imgur info
|
||||
CLIENT_ID = 'xxxxx' # Tautulli Settings > Notifications > Imgur Client ID
|
||||
ALBUM_ID = '7JeSw' # http://imgur.com/a/7JeSw <--- 7JeSw is the ablum_id
|
||||
CLIENT_ID = 'xxxxx' # Tautulli Settings > Notifications > Imgur Client ID
|
||||
ALBUM_ID = '7JeSw' # http://imgur.com/a/7JeSw <--- 7JeSw is the ablum_id
|
||||
|
||||
# Local info
|
||||
SHOW_PATH = 'D:\\Shows\\'
|
||||
|
||||
## /Edit ##
|
||||
# ## /Edit ##
|
||||
|
||||
|
||||
class IMGURINFO(object):
|
||||
def __init__(self, data=None):
|
||||
@ -28,6 +37,7 @@ class IMGURINFO(object):
|
||||
self.link = d['link']
|
||||
self.description = d['description']
|
||||
|
||||
|
||||
def get_imgur():
|
||||
url = "https://api.imgur.com/3/album/{ALBUM_ID}/images".format(ALBUM_ID=ALBUM_ID)
|
||||
headers = {'authorization': 'Client-ID {}'.format(CLIENT_ID)}
|
||||
@ -35,6 +45,7 @@ def get_imgur():
|
||||
imgur_dump = r.json()
|
||||
return[IMGURINFO(data=d) for d in imgur_dump['data']]
|
||||
|
||||
|
||||
for x in get_imgur():
|
||||
# Check if Show directory exists
|
||||
if os.path.exists(os.path.join(SHOW_PATH, x.description)):
|
||||
@ -43,6 +54,6 @@ for x in get_imgur():
|
||||
print("Poster for {} was already downloaded or filename already exists, skipping.".format(x.description))
|
||||
else:
|
||||
print("Downloading poster for {}.".format(x.description))
|
||||
urllib.urlretrieve(x.link, '{}.jpg'.format((os.path.join(SHOW_PATH, x.description, x.description))))
|
||||
urllib.request.urlretrieve(x.link, '{}.jpg'.format((os.path.join(SHOW_PATH, x.description, x.description))))
|
||||
else:
|
||||
print("{} - {} did not match your library.".format(x.description, x.link))
|
||||
|
@ -1,5 +1,7 @@
|
||||
'''
|
||||
Build playlist from popular tracks.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Build playlist from popular tracks.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@ -13,17 +15,21 @@ optional arguments:
|
||||
|
||||
* LIBRARY_EXCLUDE are excluded from libraries choice.
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
import requests
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
import argparse
|
||||
import random
|
||||
|
||||
# Edit
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxxx'
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
|
||||
LIBRARY_EXCLUDE = ['Audio Books', 'Podcasts', 'Soundtracks']
|
||||
DEFAULT_NAME = 'Popular Music Playlist'
|
||||
@ -31,7 +37,17 @@ DEFAULT_NAME = 'Popular Music Playlist'
|
||||
# /Edit
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
music_sections = [x.title for x in plex.library.sections() if x.type == 'artist' and x.title not in LIBRARY_EXCLUDE]
|
||||
@ -46,8 +62,8 @@ def fetch(path):
|
||||
|
||||
header = {'Accept': 'application/json'}
|
||||
params = {'X-Plex-Token': PLEX_TOKEN,
|
||||
'includePopularLeaves': '1'
|
||||
}
|
||||
'includePopularLeaves': '1'
|
||||
}
|
||||
|
||||
r = requests.get(url + path, headers=header, params=params, verify=False)
|
||||
return r.json()['MediaContainer']['Metadata'][0]['PopularLeaves']['Metadata']
|
||||
@ -84,7 +100,7 @@ if __name__ == "__main__":
|
||||
|
||||
parser.add_argument('--tracks', nargs='?', default=False, type=int, metavar='',
|
||||
help='Specify the track length you would like the playlist.')
|
||||
parser.add_argument('--random',nargs='?', default=False, type=int, metavar='',
|
||||
parser.add_argument('--random', nargs='?', default=False, type=int, metavar='',
|
||||
help='Randomly select N artists.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
@ -115,7 +131,7 @@ if __name__ == "__main__":
|
||||
|
||||
if opts.tracks and opts.random:
|
||||
playlist = random.sample((playlist), opts.tracks)
|
||||
|
||||
|
||||
elif opts.tracks and not opts.random:
|
||||
playlist = playlist[:opts.tracks]
|
||||
|
||||
|
@ -1,23 +1,46 @@
|
||||
'''
|
||||
Download theme songs from Plex TV Shows. Theme songs are mp3 and named by shows as displayed by Plex.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Download theme songs from Plex TV Shows.
|
||||
|
||||
Theme songs are mp3 and named by shows as displayed by Plex.
|
||||
Songs are saved in a 'Theme Songs' directory located in script's path.
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
# pip install plexapi
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import requests
|
||||
|
||||
## Edit ##
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxx'
|
||||
TV_LIBRARY = 'TV Shows' # Name of your TV Show library
|
||||
## /Edit ##
|
||||
# ## Edit ##
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
TV_LIBRARY = 'TV Shows' # Name of your TV Show library
|
||||
# ## /Edit ##
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
# Theme Songs url
|
||||
themes_url = 'http://tvthemes.plexapp.com/{}.mp3'
|
||||
@ -30,10 +53,10 @@ if not os.path.isdir(out_path):
|
||||
# Get episodes from TV Shows
|
||||
for show in plex.library.section(TV_LIBRARY).all():
|
||||
# Remove special characters from name
|
||||
filename = '{}.mp3'.format(re.sub('\W+',' ', show.title))
|
||||
filename = '{}.mp3'.format(re.sub('\W+', ' ', show.title))
|
||||
# Set output path
|
||||
theme_path = os.path.join(out_path, filename)
|
||||
# Get tvdb_if from first episode, no need to go through all episodes
|
||||
tvdb_id = show.episodes()[0].guid.split('/')[2]
|
||||
# Download theme song to output path
|
||||
urllib.urlretrieve(themes_url.format(tvdb_id), theme_path)
|
||||
urllib.request.urlretrieve(themes_url.format(tvdb_id), theme_path)
|
||||
|
@ -1,28 +1,18 @@
|
||||
"""
|
||||
Delete all playlists from Plex using PlexAPI
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Delete all playlists from Plex.
|
||||
|
||||
|
||||
https://github.com/mjs7231/python-plexapi
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from plexapi.server import PlexServer
|
||||
import requests
|
||||
|
||||
baseurl = 'http://localhost:32400'
|
||||
token = 'XXXXXXXX'
|
||||
plex = PlexServer(baseurl, token)
|
||||
|
||||
tmp_lst = []
|
||||
|
||||
|
||||
for playlist in plex.playlists():
|
||||
tmp = playlist.key
|
||||
split = tmp.split('/playlists/')
|
||||
tmp_lst += [split[1]]
|
||||
|
||||
for i in tmp_lst:
|
||||
try:
|
||||
r = requests.delete('{}/playlists/{}?X-Plex-Token={}'.format(baseurl,i,token))
|
||||
print(r)
|
||||
|
||||
except Exception as e:
|
||||
print e
|
||||
playlist.delete()
|
||||
|
@ -1,13 +0,0 @@
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
PLEX_TOKEN = 'xxxxx'
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
sections = plex.library.sections()
|
||||
titles = [titles for libraries in sections for titles in libraries.search('star')]
|
||||
for title in titles:
|
||||
try:
|
||||
print(''.join([x.file for x in title.iterParts()]))
|
||||
except Exception:
|
||||
pass
|
@ -1,13 +1,20 @@
|
||||
"""
|
||||
Description: Purge Tautulli users that no longer exist as a friend in Plex
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, plexapi
|
||||
"""
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import requests
|
||||
"""Purge Tautulli users that no longer exist as a friend in Plex.
|
||||
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, plexapi, python3.6+
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from requests import Session
|
||||
from plexapi.server import CONFIG
|
||||
from json.decoder import JSONDecodeError
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
|
||||
TAUTULLI_BASE_URL = ''
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_API_KEY = ''
|
||||
|
||||
PLEX_USERNAME = ''
|
||||
@ -16,31 +23,38 @@ PLEX_PASSWORD = ''
|
||||
# Do you want to back up the database before deleting?
|
||||
BACKUP_DB = True
|
||||
|
||||
# Do you want to back up the database before deleting?
|
||||
BACKUP_DB = True
|
||||
|
||||
# Do you want to back up the database before deleting?
|
||||
BACKUP_DB = True
|
||||
|
||||
# Do not edit past this line #
|
||||
|
||||
# Grab config vars if not set in script
|
||||
TAUTULLI_URL = TAUTULLI_URL or CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
TAUTULLI_API_KEY = TAUTULLI_API_KEY or CONFIG.data['auth'].get('tautulli_apikey')
|
||||
PLEX_USERNAME = PLEX_USERNAME or CONFIG.data['auth'].get('myplex_username')
|
||||
PLEX_PASSWORD = PLEX_PASSWORD or CONFIG.data['auth'].get('myplex_password')
|
||||
|
||||
account = MyPlexAccount(PLEX_USERNAME, PLEX_PASSWORD)
|
||||
|
||||
payload = {'apikey': TAUTULLI_API_KEY, 'cmd': 'get_user_names'}
|
||||
tautulli_users = requests.get('http://{}/api/v2'
|
||||
.format(TAUTULLI_BASE_URL), params=payload).json()['response']['data']
|
||||
session = Session()
|
||||
session.params = {'apikey': TAUTULLI_API_KEY}
|
||||
formatted_url = f'{TAUTULLI_URL}/api/v2'
|
||||
|
||||
request = session.get(formatted_url, params={'cmd': 'get_user_names'})
|
||||
|
||||
tautulli_users = None
|
||||
try:
|
||||
tautulli_users = request.json()['response']['data']
|
||||
except JSONDecodeError:
|
||||
exit("Error talking to Tautulli API, please check your TAUTULLI_URL")
|
||||
|
||||
plex_friend_ids = [friend.id for friend in account.users()]
|
||||
tautulli_user_ids = [user['user_id'] for user in tautulli_users]
|
||||
|
||||
removed_user_ids = [user_id for user_id in tautulli_user_ids if user_id not in plex_friend_ids]
|
||||
plex_friend_ids.extend((0, int(account.id)))
|
||||
removed_users = [user for user in tautulli_users if user['user_id'] not in plex_friend_ids]
|
||||
|
||||
if BACKUP_DB:
|
||||
payload['cmd'] = 'backup_db'
|
||||
backup = requests.get('http://{}/api/v2'.format(TAUTULLI_BASE_URL), params=payload)
|
||||
backup = session.get(formatted_url, params={'cmd': 'backup_db'})
|
||||
|
||||
if removed_user_ids:
|
||||
payload['cmd'] = 'delete_user'
|
||||
|
||||
for user_id in removed_user_ids:
|
||||
payload['user_id'] = user_id
|
||||
remove_user = requests.get('http://{}/api/v2'.format(TAUTULLI_BASE_URL), params=payload)
|
||||
if removed_users:
|
||||
for user in removed_users:
|
||||
removed_user = session.get(formatted_url, params={'cmd': 'delete_user', 'user_id': user['user_id']})
|
||||
print(f"Removed {user['friendly_name']} from Tautulli")
|
||||
else:
|
||||
print('No users to remove')
|
||||
|
52
utility/recently_added_collection.py
Normal file
52
utility/recently_added_collection.py
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Automatically add a movie to a collection based on release date.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi
|
||||
# Tautulli script trigger:
|
||||
# * Notify on recently added
|
||||
# Tautulli script conditions:
|
||||
# * Filter which media to add to collection.
|
||||
# [ Media Type | is | movie ]
|
||||
# [ Library Name | is | Movies ]
|
||||
# Tautulli script arguments:
|
||||
# * Recently Added:
|
||||
# --rating_key {rating_key} --collection "New Releases" --days 180
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
import argparse
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
|
||||
# Environmental Variables
|
||||
PLEX_URL = os.getenv('PLEX_URL', PLEX_URL)
|
||||
PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--rating_key', required=True, type=int)
|
||||
parser.add_argument('--collection', required=True)
|
||||
parser.add_argument('--days', required=True, type=int)
|
||||
opts = parser.parse_args()
|
||||
|
||||
threshold_date = datetime.now() - timedelta(days=opts.days)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
movie = plex.fetchItem(opts.rating_key)
|
||||
|
||||
if movie.originallyAvailableAt >= threshold_date:
|
||||
movie.addCollection(opts.collection)
|
||||
print("Added collection '{}' to '{}'.".format(opts.collection, movie.title.encode('UTF-8')))
|
||||
|
||||
for m in movie.section().search(collection=opts.collection):
|
||||
if m.originallyAvailableAt < threshold_date:
|
||||
m.removeCollection(opts.collection)
|
||||
print("Removed collection '{}' from '{}'.".format(opts.collection, m.title.encode('UTF-8')))
|
@ -1,31 +1,52 @@
|
||||
'''
|
||||
Refresh the next episode of show once current episode is watched.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Refresh the next episode of show once current episode is watched.
|
||||
|
||||
Check Tautulli's Watched Percent in Tautulli > Settings > General
|
||||
|
||||
1. Tautulli > Settings > Notification Agents > Scripts > Bell icon:
|
||||
[X] Notify on watched
|
||||
2. Tautulli > Settings > Notification Agents > Scripts > Gear icon:
|
||||
Enter the "Script folder" where you save the script.
|
||||
Watched: refresh_next_episode.py
|
||||
1. Tautulli > Settings > Notification Agents > Script > Script Triggers:
|
||||
[√] watched
|
||||
2. Tautulli > Settings > Notification Agents > Script > Gear icon:
|
||||
Enter the "Script Folder" where you save the script.
|
||||
Select "refresh_next_episode.py" in "Script File".
|
||||
Save
|
||||
3. Tautulli > Settings > Notifications > Script > Script Arguments:
|
||||
{show_name} {episode_num00} {season_num00}
|
||||
3. Tautulli > Settings > Notification Agents > Script > Script Arguments > Watched:
|
||||
<episode>{show_name} {episode_num00} {season_num00}</episode>
|
||||
|
||||
'''
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import requests
|
||||
import sys
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
# pip install plexapi
|
||||
|
||||
|
||||
baseurl = 'http://localhost:32400'
|
||||
token = 'XXXXXX' # Plex Token
|
||||
plex = PlexServer(baseurl, token)
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
|
||||
sess = requests.Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
sess.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied
|
||||
# with OpenSSL.
|
||||
if sess.verify is False:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
|
||||
show_name = sys.argv[1]
|
||||
next_ep_num = int(sys.argv[2])
|
||||
season_num = int(sys.argv[3])
|
||||
TV_LIBRARY = 'My TV Shows' # Name of your TV Shows library
|
||||
TV_LIBRARY = 'My TV Shows' # Name of your TV Shows library
|
||||
|
||||
current_season = season_num - 1
|
||||
|
||||
|
@ -1,98 +1,125 @@
|
||||
"""
|
||||
Unshare or Remove users who have been inactive for X days. Prints out last seen for all users.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
Just run.
|
||||
"""Unshare or Remove users who have been inactive for X days. Prints out last seen for all users.
|
||||
|
||||
Just run.
|
||||
|
||||
Comment out `remove_friend(username)` and `unshare(username)` to test.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import datetime
|
||||
import time
|
||||
from plexapi.server import PlexServer
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from sys import exit
|
||||
from requests import Session
|
||||
from datetime import datetime
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'xxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8182/' # Your Tautulli URL
|
||||
# EDIT THESE SETTINGS #
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
|
||||
PLEX_TOKEN = 'xxxx'
|
||||
PLEX_URL = 'http://localhost:32400'
|
||||
REMOVE_LIMIT = 30 # Days
|
||||
UNSHARE_LIMIT = 15 # Days
|
||||
|
||||
REMOVE_LIMIT = 30 # days
|
||||
UNSHARE_LIMIT = 15 # days
|
||||
USERNAME_IGNORE = ['user1', 'username2']
|
||||
IGNORE_NEVER_SEEN = True
|
||||
DRY_RUN = True
|
||||
# EDIT THESE SETTINGS #
|
||||
|
||||
USER_IGNORE = ('user1')
|
||||
##/EDIT THESE SETTINGS ##
|
||||
# CODE BELOW #
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
PLEX_URL = PLEX_URL or CONFIG.data['auth'].get('server_baseurl')
|
||||
PLEX_TOKEN = PLEX_TOKEN or CONFIG.data['auth'].get('server_token')
|
||||
TAUTULLI_URL = TAUTULLI_URL or CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
TAUTULLI_APIKEY = TAUTULLI_APIKEY or CONFIG.data['auth'].get('tautulli_apikey')
|
||||
USERNAME_IGNORE = [username.lower() for username in USERNAME_IGNORE]
|
||||
SESSION = Session()
|
||||
# Ignore verifying the SSL certificate
|
||||
SESSION.verify = False # '/path/to/certfile'
|
||||
# If verify is set to a path to a directory,
|
||||
# the directory must have been processed using the c_rehash utility supplied with OpenSSL.
|
||||
if not SESSION.verify:
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
from urllib3 import disable_warnings
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
disable_warnings(InsecureRequestWarning)
|
||||
|
||||
sections_lst = [x.title for x in plex.library.sections()]
|
||||
today = time.mktime(datetime.datetime.today().timetuple())
|
||||
SERVER = PlexServer(baseurl=PLEX_URL, token=PLEX_TOKEN, session=SESSION)
|
||||
ACCOUNT = SERVER.myPlexAccount()
|
||||
SECTIONS = [section.title for section in SERVER.library.sections()]
|
||||
PLEX_USERS = {user.id: user.title for user in ACCOUNT.users()}
|
||||
PLEX_USERS.update({int(ACCOUNT.id): ACCOUNT.title})
|
||||
IGNORED_UIDS = [uid for uid, username in PLEX_USERS.items() if username.lower() in USERNAME_IGNORE]
|
||||
IGNORED_UIDS.extend((int(ACCOUNT.id), 0))
|
||||
# Get the Tautulli history.
|
||||
PARAMS = {
|
||||
'cmd': 'get_users_table',
|
||||
'order_column': 'last_seen',
|
||||
'order_dir': 'asc',
|
||||
'length': 200,
|
||||
'apikey': TAUTULLI_APIKEY
|
||||
}
|
||||
TAUTULLI_USERS = []
|
||||
try:
|
||||
GET = SESSION.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=PARAMS).json()['response']['data']['data']
|
||||
for user in GET:
|
||||
if user['user_id'] in IGNORED_UIDS:
|
||||
continue
|
||||
elif IGNORE_NEVER_SEEN and not user['last_seen']:
|
||||
continue
|
||||
TAUTULLI_USERS.append(user)
|
||||
except Exception as e:
|
||||
exit("Tautulli API 'get_users_table' request failed. Error: {}.".format(e))
|
||||
|
||||
|
||||
def get_users_table():
|
||||
# Get the Tautulli history.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
'cmd': 'get_users_table',
|
||||
'order_column': 'last_seen',
|
||||
'order_dir': 'asc'}
|
||||
|
||||
try:
|
||||
r = requests.get(TAUTULLI_URL.rstrip('/') + '/api/v2', params=payload)
|
||||
response = r.json()
|
||||
|
||||
res_data = response['response']['data']['data']
|
||||
return [data for data in res_data if data['last_seen']]
|
||||
|
||||
except Exception as e:
|
||||
print("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
def time_format(total_seconds):
|
||||
# Display user's last history entry
|
||||
days = total_seconds // 86400
|
||||
hours = (total_seconds - days * 86400) // 3600
|
||||
minutes = (total_seconds - days * 86400 - hours * 3600) // 60
|
||||
seconds = total_seconds - days * 86400 - hours * 3600 - minutes * 60
|
||||
result = ("{} day{}, ".format(days, "s" if days != 1 else "") if days else "") + \
|
||||
("{} hour{}, ".format(hours, "s" if hours != 1 else "") if hours else "") + \
|
||||
("{} minute{}, ".format(minutes, "s" if minutes != 1 else "") if minutes else "") + \
|
||||
("{} second{}, ".format(seconds, "s" if seconds != 1 else "") if seconds else "")
|
||||
return result.strip().rstrip(',')
|
||||
|
||||
|
||||
def unshare(user):
|
||||
print('{user} has reached inactivity limit. Unsharing.'.format(user=user))
|
||||
plex.myPlexAccount().updateFriend(user=user, server=plex, removeSections=True, sections=sections_lst)
|
||||
print('Unshared all libraries from {user}.'.format(user=user))
|
||||
NOW = datetime.today()
|
||||
for user in TAUTULLI_USERS:
|
||||
OUTPUT = []
|
||||
USERNAME = user['friendly_name']
|
||||
UID = user['user_id']
|
||||
if not user['last_seen']:
|
||||
TOTAL_SECONDS = None
|
||||
OUTPUT = '{} has never used the server'.format(USERNAME)
|
||||
else:
|
||||
TOTAL_SECONDS = int((NOW - datetime.fromtimestamp(user['last_seen'])).total_seconds())
|
||||
OUTPUT = '{} was last seen {} ago'.format(USERNAME, time_format(TOTAL_SECONDS))
|
||||
|
||||
if UID not in PLEX_USERS.keys():
|
||||
print('{}, and exists in Tautulli but does not exist in Plex. Skipping.'.format(OUTPUT))
|
||||
continue
|
||||
|
||||
def remove_friend(user):
|
||||
print('{user} has reached inactivity limit. Removing.'.format(user=user))
|
||||
plex.myPlexAccount().removeFriend(user)
|
||||
print('Removed {user}.'.format(user=user))
|
||||
TOTAL_SECONDS = TOTAL_SECONDS or 86400 * UNSHARE_LIMIT
|
||||
if TOTAL_SECONDS >= (REMOVE_LIMIT * 86400):
|
||||
if DRY_RUN:
|
||||
print('{}, and would be removed.'.format(OUTPUT))
|
||||
else:
|
||||
print('{}, and has reached their shareless threshold. Removing.'.format(OUTPUT))
|
||||
ACCOUNT.removeFriend(PLEX_USERS[UID])
|
||||
elif TOTAL_SECONDS >= (UNSHARE_LIMIT * 86400):
|
||||
if DRY_RUN:
|
||||
print('{}, and would unshare libraries.'.format(OUTPUT))
|
||||
else:
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
user_tables = get_users_table()
|
||||
|
||||
for user in user_tables:
|
||||
last_seen = (today - user['last_seen']) / 24 / 60 / 60
|
||||
if int(last_seen) != 0:
|
||||
last_seen = int(last_seen)
|
||||
|
||||
username = user['friendly_name']
|
||||
if username not in USER_IGNORE:
|
||||
if last_seen > REMOVE_LIMIT:
|
||||
print('{} was last seen {} days ago. Removing.'.format(username, last_seen))
|
||||
remove_friend(username)
|
||||
elif last_seen > UNSHARE_LIMIT:
|
||||
print('{} was last seen {} days ago. Unshsring.'.format(username, last_seen))
|
||||
unshare(username)
|
||||
elif last_seen > 1:
|
||||
print('{} was last seen {} days ago.'.format(username, last_seen))
|
||||
elif int(last_seen) == 1:
|
||||
print('{} was last seen yesterday.'.format(username))
|
||||
else:
|
||||
hours_ago = last_seen * 24
|
||||
if int(hours_ago) != 0:
|
||||
hours_ago = int(hours_ago)
|
||||
print('{} was last seen {} hours ago.'.format(username, hours_ago))
|
||||
for server in ACCOUNT.user(PLEX_USERS[UID]).servers:
|
||||
if server.machineIdentifier == SERVER.machineIdentifier and server.sections():
|
||||
print('{}, and has reached their inactivity limit. Unsharing.'.format(OUTPUT))
|
||||
ACCOUNT.updateFriend(PLEX_USERS[UID], SERVER, SECTIONS, removeSections=True)
|
||||
else:
|
||||
minutes_ago = int(hours_ago * 60)
|
||||
print('{} was last seen {} minutes ago.'.format(username, minutes_ago))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
print("{}, has already been unshared, but has not reached their shareless threshold."
|
||||
"Skipping.".format(OUTPUT))
|
||||
|
23
utility/remove_movie_collections.py
Normal file
23
utility/remove_movie_collections.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Removes ALL collections from ALL movies.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
### EDIT SETTINGS ###
|
||||
|
||||
PLEX_URL = "http://localhost:32400"
|
||||
PLEX_TOKEN = "xxxxxxxxxx"
|
||||
MOVIE_LIBRARY_NAME = "Movies"
|
||||
|
||||
|
||||
## CODE BELOW ##
|
||||
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
for movie in plex.library.section(MOVIE_LIBRARY_NAME).all():
|
||||
movie.removeCollection([c.tag for c in movie.collections])
|
@ -1,21 +1,27 @@
|
||||
"""
|
||||
Find Movies that have been watched by a list of users.
|
||||
If all users have watched movie than delete.
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Find and delete Movies that have been watched by a list of users.
|
||||
|
||||
Deletion is prompted
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import input
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ##
|
||||
# ## EDIT THESE SETTINGS ##
|
||||
TAUTULLI_APIKEY = 'xxxxxxxx' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
LIBRARY_NAMES = ['My Movies'] # Whatever your movie libraries are called.
|
||||
USER_LST = ['Joe', 'Alex'] # Name of users
|
||||
LIBRARY_NAMES = ['My Movies'] # Whatever your movie libraries are called.
|
||||
USER_LST = ['Joe', 'Alex'] # Name of users
|
||||
|
||||
|
||||
class UserHIS(object):
|
||||
def __init__(self, data=None):
|
||||
@ -73,7 +79,7 @@ def get_history(user, start, length):
|
||||
|
||||
|
||||
def delete_files(tmp_lst):
|
||||
del_file = raw_input('Delete all watched files? (yes/no)').lower()
|
||||
del_file = input('Delete all watched files? (yes/no)').lower()
|
||||
if del_file.startswith('y'):
|
||||
for x in tmp_lst:
|
||||
print("Removing {}".format(os.path.dirname(x)))
|
||||
@ -81,6 +87,7 @@ def delete_files(tmp_lst):
|
||||
else:
|
||||
print('Ok. doing nothing.')
|
||||
|
||||
|
||||
movie_dict = {}
|
||||
movie_lst = []
|
||||
delete_lst = []
|
||||
@ -118,9 +125,8 @@ for user in USER_LST:
|
||||
pass
|
||||
|
||||
for movie_dict in movie_lst:
|
||||
for key, value in movie_dict.items():
|
||||
if value == USER_LST:
|
||||
print(u"{} has been watched by {}".format(movie_dict['title']," & ".join(USER_LST)))
|
||||
delete_lst.append(movie_dict['file'])
|
||||
if set(USER_LST) == set(movie_dict['watched_by']):
|
||||
print(u"{} has been watched by {}".format(movie_dict['title'], " & ".join(USER_LST)))
|
||||
delete_lst.append(movie_dict['file'])
|
||||
|
||||
delete_files(delete_lst)
|
||||
|
54
utility/rename_seasons.py
Normal file
54
utility/rename_seasons.py
Normal file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Description: Rename season title for TV shows on Plex.
|
||||
# Author: /u/SwiftPanda16
|
||||
# Requires: plexapi
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
|
||||
### EDIT SETTINGS ###
|
||||
|
||||
PLEX_URL = "http://localhost:32400"
|
||||
PLEX_TOKEN = "xxxxxxxxxx"
|
||||
|
||||
TV_SHOW_LIBRARY = "TV Shows"
|
||||
TV_SHOW_NAME = "Sailor Moon"
|
||||
SEASON_MAPPINGS = {
|
||||
"Season 1": "Sailor Moon", # Season 1 will be renamed to Sailor Moon
|
||||
"Season 2": "Sailor Moon R", # Season 2 will be renamed to Sailor Moon R
|
||||
"Season 3": "Sailor Moon S", # etc.
|
||||
"Season 4": "Sailor Moon SuperS",
|
||||
"Season 5": "Sailor Moon Sailor Stars",
|
||||
"Bad Season Title": "", # Blank string "" to reset season title
|
||||
}
|
||||
|
||||
|
||||
## CODE BELOW ##
|
||||
|
||||
def main():
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN)
|
||||
|
||||
show = plex.library.section(TV_SHOW_LIBRARY).get(TV_SHOW_NAME)
|
||||
print("Found TV show '{}' in the '{}' library on Plex.".format(TV_SHOW_NAME, TV_SHOW_LIBRARY))
|
||||
|
||||
for season in show.seasons():
|
||||
old_season_title = season.title
|
||||
new_season_title = SEASON_MAPPINGS.get(season.title)
|
||||
if new_season_title:
|
||||
season.edit(**{"title.value": new_season_title, "title.locked": 1})
|
||||
print("'{}' renamed to '{}'.".format(old_season_title, new_season_title))
|
||||
elif new_season_title == "":
|
||||
season.edit(**{"title.value": new_season_title, "title.locked": 0})
|
||||
print("'{}' reset to '{}'.".format(old_season_title, season.reload().title))
|
||||
else:
|
||||
print("No mapping for '{}'. Season not renamed.".format(old_season_title))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
print("Done.")
|
BIN
utility/spoilers.png
Normal file
BIN
utility/spoilers.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -1,5 +1,7 @@
|
||||
"""
|
||||
Share functions from https://gist.github.com/JonnyWong16/f8139216e2748cb367558070c1448636
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Share functions from https://gist.github.com/JonnyWong16/f8139216e2748cb367558070c1448636
|
||||
|
||||
Once user stream count hits LIMIT they are unshared from libraries expect for banned library.
|
||||
Once user stream count is below LIMIT and banned library video is watched their shares are restored.
|
||||
@ -29,7 +31,6 @@ Tautulli will continue displaying that user is watching after unshare is execute
|
||||
Tautulli will update after ~5 minutes and no longer display user's stream in ACTIVITY.
|
||||
Tautulli will think that user has stopped.
|
||||
|
||||
|
||||
Create new library with one video.
|
||||
Name library and video whatever you want.
|
||||
|
||||
@ -48,8 +49,12 @@ Clear user history for banned video to remove violation counts and run manually
|
||||
Concurrent stream count is the trigger. Trigger can be anything you want.
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
from builtins import str
|
||||
from builtins import object
|
||||
import requests
|
||||
import sys
|
||||
from xml.dom import minidom
|
||||
@ -58,7 +63,7 @@ import email.utils
|
||||
import smtplib
|
||||
|
||||
|
||||
## EDIT THESE SETTINGS ###
|
||||
# ## EDIT THESE SETTINGS ###
|
||||
|
||||
TAUTULLI_APIKEY = 'XXXXXX' # Your Tautulli API key
|
||||
TAUTULLI_URL = 'http://localhost:8181/' # Your Tautulli URL
|
||||
@ -73,8 +78,8 @@ SERVER_ID = "XXXXX" # Example: https://i.imgur.com/EjaMTUk.png
|
||||
# UserID2: [LibraryID1, LibraryID2]}
|
||||
|
||||
USER_LIBRARIES = {123456: [123456, 123456, 123456, 123456, 123456, 123456]}
|
||||
BAN_LIBRARY = {123456: [123456]} # {UserID1: [LibraryID1], UserID2: [LibraryID1]}
|
||||
BAN_RATING = 123456 # Banned rating_key to check if it's been watched.
|
||||
BAN_LIBRARY = {123456: [123456]} # {UserID1: [LibraryID1], UserID2: [LibraryID1]}
|
||||
BAN_RATING = 123456 # Banned rating_key to check if it's been watched.
|
||||
|
||||
LIMIT = 3
|
||||
VIOLATION_LIMIT = 3
|
||||
@ -98,15 +103,15 @@ BODY_TEXT = """\
|
||||
"""
|
||||
|
||||
# Email settings
|
||||
name = '' # Your name
|
||||
sender = '' # From email address
|
||||
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
|
||||
name = '' # Your name
|
||||
sender = '' # From email address
|
||||
email_server = 'smtp.gmail.com' # Email server (Gmail: smtp.gmail.com)
|
||||
email_port = 587 # Email port (Gmail: 587)
|
||||
email_username = '' # Your email username
|
||||
email_password = '' # Your email password
|
||||
email_username = '' # Your email username
|
||||
email_password = '' # Your email password
|
||||
|
||||
|
||||
## DO NOT EDIT BELOW ##
|
||||
# ## DO NOT EDIT BELOW ##
|
||||
|
||||
class Activity(object):
|
||||
def __init__(self, data=None):
|
||||
@ -120,6 +125,7 @@ class Activity(object):
|
||||
self.transcode_key = d['transcode_key']
|
||||
self.state = d['state']
|
||||
|
||||
|
||||
class Users(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
@ -127,6 +133,7 @@ class Users(object):
|
||||
self.user_id = d['user_id']
|
||||
self.friendly_name = d['friendly_name']
|
||||
|
||||
|
||||
def get_user(user_id):
|
||||
# Get the user list from Tautulli.
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -165,6 +172,7 @@ def get_history(user_id, bankey):
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_history' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def share(user_id, ban):
|
||||
|
||||
headers = {"X-Plex-Token": PLEX_TOKEN,
|
||||
@ -198,6 +206,7 @@ def share(user_id, ban):
|
||||
|
||||
return
|
||||
|
||||
|
||||
def unshare(user_id):
|
||||
|
||||
headers = {"X-Plex-Token": PLEX_TOKEN,
|
||||
@ -211,7 +220,7 @@ def unshare(user_id):
|
||||
return
|
||||
|
||||
elif r.status_code == 400:
|
||||
print r.content
|
||||
print(r.content)
|
||||
return
|
||||
|
||||
elif r.status_code == 200:
|
||||
@ -236,6 +245,7 @@ def unshare(user_id):
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_activity():
|
||||
# Get the user IP list from Tautulli
|
||||
payload = {'apikey': TAUTULLI_APIKEY,
|
||||
@ -250,6 +260,7 @@ def get_activity():
|
||||
except Exception as e:
|
||||
sys.stderr.write("Tautulli API 'get_activity' request failed: {0}.".format(e))
|
||||
|
||||
|
||||
def send_notification(to=None, friendly=None, val_cnt=None, val_tot=None, mess=None):
|
||||
# Format notification text
|
||||
try:
|
||||
@ -289,9 +300,9 @@ if __name__ == "__main__":
|
||||
try:
|
||||
if act_lst.count(user) >= LIMIT:
|
||||
# Trigger for first and next violation
|
||||
unshare(user) # Remove libraries
|
||||
share(user, BAN) # Share banned library
|
||||
sys.stdout.write("Shared BAN_LIBRARY with user {0}".format(i))
|
||||
unshare(user) # Remove libraries
|
||||
share(user, BAN) # Share banned library
|
||||
sys.stdout.write("Shared BAN_LIBRARY with user {0}".format(user))
|
||||
if type(history) is int:
|
||||
# Next violation, history of banned video.
|
||||
send_notification(mail_add, friendly, history, VIOLATION_LIMIT, FIRST_WARN)
|
||||
@ -302,10 +313,10 @@ if __name__ == "__main__":
|
||||
elif type(history) is int:
|
||||
# Trigger to share
|
||||
if share(user, UNBAN) == 400:
|
||||
exit() # User has history of watching banned video but libraries are already restored.
|
||||
exit() # User has history of watching banned video but libraries are already restored.
|
||||
else:
|
||||
unshare(user) # Remove banned library
|
||||
share(user, UNBAN) # Restore libraries
|
||||
unshare(user) # Remove banned library
|
||||
share(user, UNBAN) # Restore libraries
|
||||
elif history == 'ban':
|
||||
# Trigger for ban
|
||||
unshare(user)
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""
|
||||
Description: Sync the watch status from one user to another. Either by user or user/libraries
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Sync the watch status from one Plex or Tautulli user to other users across any owned server.
|
||||
|
||||
Author: Blacktwin
|
||||
Requires: requests, plexapi, argparse
|
||||
|
||||
@ -30,137 +33,552 @@ Script Arguments:
|
||||
Taultulli > Settings > Notification Agents > New Script > Script Arguments:
|
||||
|
||||
Select: Notify on Watched
|
||||
Arguments: --ratingKey {rating_key} --userTo "Username2" "Username3" --userFrom {username}
|
||||
Arguments: --ratingKey {rating_key} --userFrom Tautulli=Tautulli --userTo "Username2=Server1" "Username3=Server1"
|
||||
|
||||
Save
|
||||
Close
|
||||
|
||||
Example:
|
||||
Set in Tautulli in script notification agent or run manually
|
||||
Set in Tautulli in script notification agent (above) or run manually (below)
|
||||
|
||||
plex_api_share.py --userFrom USER1 --userTo USER2 --libraries Movies
|
||||
- Synced watch status of {title from library} to {USER2}'s account.
|
||||
sync_watch_status.py --userFrom USER1=Server1 --userTo USER2=Server1 --libraries Movies
|
||||
- Synced watch status from Server1 {title from library} to {USER2}'s account on Server1.
|
||||
|
||||
plex_api_share.py --userFrom USER1 --userTo USER2 USER3 --allLibraries
|
||||
- Synced watch status of {title from library} to {USER2 or USER3}'s account.
|
||||
sync_watch_status.py --userFrom USER1=Server2 --userTo USER2=Server1 USER3=Server1 --libraries Movies "TV Shows"
|
||||
- Synced watch status from Server2 {title from library} to {USER2 or USER3}'s account on Server1.
|
||||
|
||||
Excluding;
|
||||
--libraries becomes excluded if --allLibraries is set
|
||||
sync_watch_status.py --userFrom USER1 --userTo USER2 --allLibraries --libraries Movies
|
||||
- Shared [all libraries but Movies] with USER.
|
||||
sync_watch_status.py --userFrom USER1=Tautulli --userTo USER2=Server1 USER3=Server2 --libraries Movies "TV Shows"
|
||||
- Synced watch statuses from Tautulli {title from library} to {USER2 or USER3}'s account on selected servers.
|
||||
|
||||
sync_watch_status.py --userFrom USER1=Tautulli --userTo USER2=Server1 USER3=Server2 --ratingKey 1234
|
||||
- Synced watch statuse of rating key 1234 from USER1's Tautulli history to {USER2 or USER3}'s account
|
||||
on selected servers.
|
||||
**Rating key must be a movie or episode. Shows and Seasons not support.... yet.
|
||||
"""
|
||||
import requests
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from builtins import object
|
||||
import argparse
|
||||
from plexapi.server import PlexServer, CONFIG
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.server import CONFIG
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# Using CONFIG file
|
||||
PLEX_URL = ''
|
||||
PLEX_TOKEN = ''
|
||||
PLEX_URL = CONFIG.data['auth'].get('server_baseurl', PLEX_URL)
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token', PLEX_TOKEN)
|
||||
TAUTULLI_URL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
|
||||
if not PLEX_TOKEN:
|
||||
PLEX_TOKEN = CONFIG.data['auth'].get('server_token')
|
||||
if not TAUTULLI_URL:
|
||||
TAUTULLI_URL = CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
if not TAUTULLI_APIKEY:
|
||||
TAUTULLI_APIKEY = CONFIG.data['auth'].get('tautulli_apikey')
|
||||
|
||||
VERIFY_SSL = False
|
||||
|
||||
|
||||
sess = requests.Session()
|
||||
sess.verify = False
|
||||
plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess)
|
||||
class Connection(object):
|
||||
def __init__(self, url=None, apikey=None, verify_ssl=False):
|
||||
self.url = url
|
||||
self.apikey = apikey
|
||||
self.session = Session()
|
||||
self.adapters = HTTPAdapter(max_retries=3,
|
||||
pool_connections=1,
|
||||
pool_maxsize=1,
|
||||
pool_block=True)
|
||||
self.session.mount('http://', self.adapters)
|
||||
self.session.mount('https://', self.adapters)
|
||||
|
||||
sections_lst = [x.title for x in plex.library.sections()]
|
||||
user_lst = [x.title for x in plex.myPlexAccount().users()]
|
||||
# Adding admin account name to list
|
||||
user_lst.append(plex.myPlexAccount().title)
|
||||
# Ignore verifying the SSL certificate
|
||||
if verify_ssl is False:
|
||||
self.session.verify = False
|
||||
# Disable the warning that the request is insecure, we know that...
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
def get_account(user):
|
||||
if user == plex.myPlexAccount().title:
|
||||
server = plex
|
||||
|
||||
class Library(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.title = d['section_name']
|
||||
self.key = d['section_id']
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
def __init__(self, data=None):
|
||||
d = data or {}
|
||||
self.type = d['media_type']
|
||||
self.grandparentTitle = d['grandparent_title']
|
||||
self.parentIndex = d['parent_media_index']
|
||||
self.index = d['media_index']
|
||||
if self.type == 'episode':
|
||||
ep_name = d['full_title'].partition('-')[-1]
|
||||
self.title = ep_name.lstrip()
|
||||
else:
|
||||
self.title = d['full_title']
|
||||
|
||||
# For History
|
||||
try:
|
||||
if d['watched_status']:
|
||||
self.watched_status = d['watched_status']
|
||||
except KeyError:
|
||||
pass
|
||||
# For Metadata
|
||||
try:
|
||||
if d["library_name"]:
|
||||
self.libraryName = d['library_name']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class Tautulli(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def _call_api(self, cmd, payload, method='GET'):
|
||||
payload['cmd'] = cmd
|
||||
payload['apikey'] = self.connection.apikey
|
||||
|
||||
try:
|
||||
response = self.connection.session.request(method, self.connection.url + '/api/v2', params=payload)
|
||||
except RequestException as e:
|
||||
print("Tautulli request failed for cmd '{}'. Invalid Tautulli URL? Error: {}".format(cmd, e))
|
||||
return
|
||||
|
||||
try:
|
||||
response_json = response.json()
|
||||
except ValueError:
|
||||
print("Failed to parse json response for Tautulli API cmd '{}'".format(cmd))
|
||||
return
|
||||
|
||||
if response_json['response']['result'] == 'success':
|
||||
return response_json['response']['data']
|
||||
else:
|
||||
error_msg = response_json['response']['message']
|
||||
print("Tautulli API cmd '{}' failed: {}".format(cmd, error_msg))
|
||||
return
|
||||
|
||||
def get_watched_history(self, user=None, section_id=None, rating_key=None, start=None, length=None):
|
||||
"""Call Tautulli's get_history api endpoint."""
|
||||
payload = {"order_column": "full_title",
|
||||
"order_dir": "asc"}
|
||||
if user:
|
||||
payload["user"] = user
|
||||
if section_id:
|
||||
payload["section_id"] = section_id
|
||||
if rating_key:
|
||||
payload["rating_key"] = rating_key
|
||||
if start:
|
||||
payload["start"] = start
|
||||
if length:
|
||||
payload["lengh"] = length
|
||||
|
||||
history = self._call_api('get_history', payload)
|
||||
|
||||
return [d for d in history['data'] if d['watched_status'] == 1]
|
||||
|
||||
def get_metadata(self, rating_key):
|
||||
"""Call Tautulli's get_metadata api endpoint."""
|
||||
payload = {"rating_key": rating_key}
|
||||
return self._call_api('get_metadata', payload)
|
||||
|
||||
def get_libraries(self):
|
||||
"""Call Tautulli's get_libraries api endpoint."""
|
||||
payload = {}
|
||||
return self._call_api('get_libraries', payload)
|
||||
|
||||
|
||||
class Plex(object):
|
||||
def __init__(self, token, url=None):
|
||||
if token and not url:
|
||||
self.account = MyPlexAccount(token)
|
||||
if token and url:
|
||||
session = Connection().session
|
||||
self.server = PlexServer(baseurl=url, token=token, session=session)
|
||||
|
||||
def admin_servers(self):
|
||||
"""Get all owned servers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data: dict
|
||||
|
||||
"""
|
||||
resources = {}
|
||||
for resource in self.account.resources():
|
||||
if 'server' in [resource.provides] and resource.owned is True:
|
||||
resources[resource.name] = resource
|
||||
|
||||
return resources
|
||||
|
||||
def all_users(self):
|
||||
"""Get all users.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data: dict
|
||||
|
||||
"""
|
||||
users = {self.account.title: self.account}
|
||||
for user in self.account.users():
|
||||
users[user.title] = user
|
||||
|
||||
return users
|
||||
|
||||
def all_sections(self):
|
||||
"""Get all sections from all owned servers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data: dict
|
||||
|
||||
"""
|
||||
data = {}
|
||||
servers = self.admin_servers()
|
||||
print("Connecting to admin server(s) for access info...")
|
||||
for name, server in servers.items():
|
||||
connect = server.connect()
|
||||
sections = {section.title: section for section in connect.library.sections()}
|
||||
data[name] = sections
|
||||
|
||||
return data
|
||||
|
||||
def users_access(self):
|
||||
"""Get users access across all owned servers.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data: dict
|
||||
|
||||
"""
|
||||
all_users = self.all_users().values()
|
||||
admin_servers = self.admin_servers()
|
||||
all_sections = self.all_sections()
|
||||
|
||||
data = {self.account.title: {"account": self.account}}
|
||||
|
||||
for user in all_users:
|
||||
if not data.get(user.title):
|
||||
servers = []
|
||||
for server in user.servers:
|
||||
if admin_servers.get(server.name):
|
||||
access = {}
|
||||
sections = {section.title: section for section in server.sections()
|
||||
if section.shared is True}
|
||||
access['server'] = {server.name: admin_servers.get(server.name)}
|
||||
access['sections'] = sections
|
||||
servers += [access]
|
||||
data[user.title] = {'account': user,
|
||||
'access': servers}
|
||||
else:
|
||||
# Admin account
|
||||
servers = []
|
||||
for name, server in admin_servers.items():
|
||||
access = {}
|
||||
sections = all_sections.get(name)
|
||||
access['server'] = {name: server}
|
||||
access['sections'] = sections
|
||||
servers += [access]
|
||||
data[user.title] = {'account': user,
|
||||
'access': servers}
|
||||
return data
|
||||
|
||||
|
||||
def connect_to_server(server_obj, user_account):
|
||||
"""Find server url and connect using user token.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
server_obj: class
|
||||
user_account: class
|
||||
|
||||
Returns
|
||||
-------
|
||||
user_connection.server: class
|
||||
|
||||
"""
|
||||
server_name = server_obj.name
|
||||
user = user_account.title
|
||||
|
||||
print('Connecting {} to {}...'.format(user, server_name))
|
||||
server_connection = server_obj.connect()
|
||||
url = server_connection._baseurl
|
||||
if user_account.title == Plex(PLEX_TOKEN).account.title:
|
||||
token = PLEX_TOKEN
|
||||
else:
|
||||
# Access Plex User's Account
|
||||
userAccount = plex.myPlexAccount().user(user)
|
||||
token = userAccount.get_token(plex.machineIdentifier)
|
||||
server = PlexServer(PLEX_URL, token)
|
||||
return server
|
||||
token = user_account.get_token(server_connection.machineIdentifier)
|
||||
|
||||
user_connection = Plex(url=url, token=token)
|
||||
|
||||
return user_connection.server
|
||||
|
||||
|
||||
def mark_watached(sectionFrom, accountTo, userTo):
|
||||
# Check sections for watched items
|
||||
for item in sectionFrom.search(unwatched=False):
|
||||
title = item.title.encode('utf-8')
|
||||
# Check movie media type
|
||||
if item.type == 'movie':
|
||||
accountTo.fetchItem(item.key).markWatched()
|
||||
print('Synced watch status of {} to {}\'s account.'.format(title, userTo))
|
||||
# Check show media type
|
||||
elif item.type == 'show':
|
||||
for episode in sectionFrom.searchEpisodes(unwatched=False, title=title):
|
||||
ep_title = episode.title.encode('utf-8')
|
||||
accountTo.fetchItem(episode.key).markWatched()
|
||||
print('Synced watch status of {} - {} to {}\'s account.'.format(title, ep_title, userTo))
|
||||
def check_users_access(access, user, server_name, libraries=None):
|
||||
"""Check user's access to server. If allowed connect.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
access: dict
|
||||
user: dict
|
||||
server_name: str
|
||||
libraries: list
|
||||
|
||||
Returns
|
||||
-------
|
||||
server_connection: class
|
||||
|
||||
"""
|
||||
try:
|
||||
_user = access.get(user)
|
||||
for access in _user['access']:
|
||||
server = access.get("server")
|
||||
# Check user access to server
|
||||
if server.get(server_name):
|
||||
server_obj = server.get(server_name)
|
||||
# If syncing by libraries, check library access
|
||||
if libraries:
|
||||
library_check = any(lib.title in access.get("sections").keys() for lib in libraries)
|
||||
# Check user access to library
|
||||
if library_check:
|
||||
server_connection = connect_to_server(server_obj, _user['account'])
|
||||
return server_connection
|
||||
|
||||
elif not library_check:
|
||||
print("User does not have access to this library.")
|
||||
# Not syncing by libraries
|
||||
else:
|
||||
server_connection = connect_to_server(server_obj, _user['account'])
|
||||
return server_connection
|
||||
# else:
|
||||
# print("User does not have access to this server: {}.".format(server_name))
|
||||
except KeyError:
|
||||
print('User name is incorrect.')
|
||||
print(", ".join(plex_admin.all_users().keys()))
|
||||
exit()
|
||||
|
||||
|
||||
def sync_watch_status(watched, section, accountTo, userTo, same_server=False):
|
||||
"""Sync watched status between two users.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
watched: list
|
||||
List of watched items either from Tautulli or Plex
|
||||
section: str
|
||||
Section title of sync from server
|
||||
accountTo: class
|
||||
User's account that will be synced to
|
||||
userTo: str
|
||||
User's server class of sync to user
|
||||
same_server: bool
|
||||
Are serverFrom and serverTo the same
|
||||
|
||||
"""
|
||||
print('Marking watched...')
|
||||
sectionTo = accountTo.library.section(section)
|
||||
for item in watched:
|
||||
try:
|
||||
if same_server:
|
||||
fetch_check = sectionTo.fetchItem(item.ratingKey)
|
||||
else:
|
||||
if item.type == 'episode':
|
||||
show_name = item.grandparentTitle
|
||||
show = sectionTo.get(show_name)
|
||||
watch_check = show.episode(season=int(item.parentIndex), episode=int(item.index))
|
||||
else:
|
||||
title = item.title
|
||||
watch_check = sectionTo.get(title)
|
||||
# .get retrieves a partial object
|
||||
# .fetchItem retrieves a full object
|
||||
fetch_check = sectionTo.fetchItem(watch_check.key)
|
||||
# If item is already watched ignore
|
||||
if not fetch_check.isPlayed:
|
||||
# todo-me should watched count be synced?
|
||||
fetch_check.markPlayed()
|
||||
title = fetch_check._prettyfilename()
|
||||
print("Synced watched status of {} to account {}...".format(title, userTo))
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
|
||||
def batching_watched(section, libtype):
|
||||
count = 100
|
||||
start = 0
|
||||
watched_lst = []
|
||||
while True:
|
||||
|
||||
if libtype == 'show':
|
||||
search_watched = section.search(libtype='episode', container_start=start, container_size=count,
|
||||
**{'show.unwatchedLeaves': False})
|
||||
else:
|
||||
search_watched = section.search(unwatched=False, container_start=start, container_size=count)
|
||||
if all([search_watched]):
|
||||
start += count
|
||||
for item in search_watched:
|
||||
if item not in watched_lst:
|
||||
watched_lst.append(item)
|
||||
continue
|
||||
elif not all([search_watched]):
|
||||
break
|
||||
start += count
|
||||
|
||||
return watched_lst
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description="Sync watch status from one user to others.",
|
||||
formatter_class=argparse.RawTextHelpFormatter)
|
||||
requiredNamed = parser.add_argument_group('required named arguments')
|
||||
parser.add_argument('--libraries', nargs='*', choices=sections_lst, metavar='library',
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s)')
|
||||
parser.add_argument('--allLibraries', action='store_true',
|
||||
help='Select all libraries.')
|
||||
parser.add_argument('--ratingKey', nargs=1,
|
||||
parser.add_argument('--libraries', nargs='*', metavar='library',
|
||||
help='Libraries to scan for watched content.')
|
||||
parser.add_argument('--ratingKey', nargs="?", type=str,
|
||||
help='Rating key of item whose watch status is to be synced.')
|
||||
requiredNamed.add_argument('--userFrom', choices=user_lst, metavar='username', required=True,
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s)')
|
||||
requiredNamed.add_argument('--userTo', nargs='*', choices=user_lst, metavar='usernames', required=True,
|
||||
help='Space separated list of case sensitive names to process. Allowed names are: \n'
|
||||
'(choices: %(choices)s)')
|
||||
requiredNamed = parser.add_argument_group('required named arguments')
|
||||
requiredNamed.add_argument('--userFrom', metavar='user=server', required=True,
|
||||
type=lambda kv: kv.split("="), default=["", ""],
|
||||
help='Select user and server to sync from')
|
||||
requiredNamed.add_argument('--userTo', nargs='*', metavar='user=server', required=True,
|
||||
type=lambda kv: kv.split("="),
|
||||
help='Select user and server to sync to.')
|
||||
|
||||
opts = parser.parse_args()
|
||||
# print(opts)
|
||||
tautulli_server = ''
|
||||
|
||||
# Create Sync-From user account
|
||||
plexFrom = get_account(opts.userFrom)
|
||||
libraries = []
|
||||
all_sections = {}
|
||||
watchedFrom = ''
|
||||
same_server = False
|
||||
count = 25
|
||||
start = 0
|
||||
plex_admin = Plex(PLEX_TOKEN)
|
||||
plex_access = plex_admin.users_access()
|
||||
|
||||
userFrom, serverFrom = opts.userFrom
|
||||
|
||||
if serverFrom == "Tautulli":
|
||||
# Create a Tautulli instance
|
||||
tautulli_server = Tautulli(Connection(url=TAUTULLI_URL.rstrip('/'),
|
||||
apikey=TAUTULLI_APIKEY,
|
||||
verify_ssl=VERIFY_SSL))
|
||||
|
||||
if serverFrom == "Tautulli" and opts.libraries:
|
||||
# Pull all libraries from Tautulli
|
||||
_sections = {}
|
||||
tautulli_sections = tautulli_server.get_libraries()
|
||||
for section in tautulli_sections:
|
||||
section_obj = Library(section)
|
||||
_sections[section_obj.title] = section_obj
|
||||
all_sections[serverFrom] = _sections
|
||||
elif serverFrom != "Tautulli" and opts.libraries:
|
||||
# Pull all libraries from admin access dict
|
||||
admin_access = plex_access.get(plex_admin.account.title).get("access")
|
||||
for server in admin_access:
|
||||
if server.get("server").get(serverFrom):
|
||||
all_sections[serverFrom] = server.get("sections")
|
||||
|
||||
# Defining libraries
|
||||
libraries = ''
|
||||
if opts.allLibraries and not opts.libraries:
|
||||
libraries = sections_lst
|
||||
elif not opts.allLibraries and opts.libraries:
|
||||
libraries = opts.libraries
|
||||
elif opts.allLibraries and opts.libraries:
|
||||
# If allLibraries is used then any libraries listed will be excluded
|
||||
if opts.libraries:
|
||||
for library in opts.libraries:
|
||||
sections_lst.remove(library)
|
||||
libraries = sections_lst
|
||||
if all_sections.get(serverFrom).get(library):
|
||||
libraries.append(all_sections.get(serverFrom).get(library))
|
||||
else:
|
||||
print("No matching library name '{}'".format(library))
|
||||
exit()
|
||||
|
||||
# Go through list of users
|
||||
for user in opts.userTo:
|
||||
# Create Sync-To user account
|
||||
plexTo = get_account(user)
|
||||
if libraries:
|
||||
# Go through Libraries
|
||||
for library in libraries:
|
||||
try:
|
||||
print('Checking library: {}'.format(library))
|
||||
# Check library for watched items
|
||||
section = plexFrom.library.section(library)
|
||||
mark_watached(section, plexTo, user)
|
||||
except Exception as e:
|
||||
if str(e).startswith('Unknown'):
|
||||
print('Library ({}) does not have a watch status.'.format(library))
|
||||
elif str(e).startswith('Invalid'):
|
||||
print('Library ({}) not shared to user: {}.'.format(library, opts.userFrom))
|
||||
elif str(e).startswith('(404)'):
|
||||
print('Library ({}) not shared to user: {}.'.format(library, user))
|
||||
else:
|
||||
print(e)
|
||||
pass
|
||||
# Check rating key from Tautulli
|
||||
elif opts.ratingKey:
|
||||
item = plexTo.fetchItem(opts.ratingKey)
|
||||
title = item.title.encode('utf-8')
|
||||
print('Syncing watch status of {} to {}\'s account.'.format(title, user))
|
||||
item.markWatched()
|
||||
# If server is Plex and synciing libraries, check access
|
||||
if serverFrom != "Tautulli" and libraries:
|
||||
print("Checking {}'s access to {}".format(userFrom, serverFrom))
|
||||
watchedFrom = check_users_access(plex_access, userFrom, serverFrom, libraries)
|
||||
|
||||
if libraries:
|
||||
print("Finding watched items in libraries...")
|
||||
plexTo = []
|
||||
|
||||
for user, server_name in opts.userTo:
|
||||
plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
|
||||
|
||||
for _library in libraries:
|
||||
watched_lst = []
|
||||
print("Checking {}'s library: '{}' watch statuses...".format(userFrom, _library.title))
|
||||
if tautulli_server:
|
||||
while True:
|
||||
# Getting all watched history for userFrom
|
||||
tt_watched = tautulli_server.get_watched_history(user=userFrom, section_id=_library.key,
|
||||
start=start, length=count)
|
||||
if all([tt_watched]):
|
||||
start += count
|
||||
for item in tt_watched:
|
||||
watched_lst.append(Metadata(item))
|
||||
continue
|
||||
elif not all([tt_watched]):
|
||||
break
|
||||
start += count
|
||||
else:
|
||||
# Check library for watched items
|
||||
sectionFrom = watchedFrom.library.section(_library.title)
|
||||
watched_lst = batching_watched(sectionFrom, _library.type)
|
||||
|
||||
for user in plexTo:
|
||||
username, server = user
|
||||
if server == serverFrom:
|
||||
same_server = True
|
||||
sync_watch_status(watched_lst, _library.title, server, username, same_server)
|
||||
|
||||
elif opts.ratingKey and serverFrom == "Tautulli":
|
||||
plexTo = []
|
||||
watched_item = []
|
||||
|
||||
if userFrom != "Tautulli":
|
||||
print("Request manually triggered to update watch status")
|
||||
tt_watched = tautulli_server.get_watched_history(user=userFrom, rating_key=opts.ratingKey)
|
||||
if tt_watched:
|
||||
watched_item = Metadata(tautulli_server.get_metadata(opts.ratingKey))
|
||||
else:
|
||||
print("Rating Key {} was not reported as watched in Tautulli for user {}".format(opts.ratingKey, userFrom))
|
||||
exit()
|
||||
|
||||
elif userFrom == "Tautulli":
|
||||
print("Request from Tautulli notification agent to update watch status")
|
||||
watched_item = Metadata(tautulli_server.get_metadata(opts.ratingKey))
|
||||
|
||||
for user, server_name in opts.userTo:
|
||||
# Check access and connect
|
||||
plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
|
||||
|
||||
for user in plexTo:
|
||||
username, server = user
|
||||
sync_watch_status([watched_item], watched_item.libraryName, server, username)
|
||||
|
||||
elif opts.ratingKey and serverFrom != "Tautulli":
|
||||
plexTo = []
|
||||
watched_item = []
|
||||
|
||||
if userFrom != "Tautulli":
|
||||
print("Request manually triggered to update watch status")
|
||||
watchedFrom = check_users_access(plex_access, userFrom, serverFrom)
|
||||
watched_item = watchedFrom.fetchItem(int(opts.ratingKey))
|
||||
if not watched_item.isPlayed:
|
||||
print("Rating Key {} was not reported as watched in Plex for user {}".format(opts.ratingKey,
|
||||
userFrom))
|
||||
exit()
|
||||
else:
|
||||
print('No libraries or rating key provided.')
|
||||
print("Use an actual user.")
|
||||
exit()
|
||||
|
||||
for user, server_name in opts.userTo:
|
||||
# Check access and connect
|
||||
plexTo.append([user, check_users_access(plex_access, user, server_name, libraries)])
|
||||
|
||||
for user in plexTo:
|
||||
username, server = user
|
||||
library = server.library.sectionByID(watched_item.librarySectionID)
|
||||
sync_watch_status([watched_item], library.title, server, username)
|
||||
|
||||
else:
|
||||
print("You aren't using this script correctly... bye!")
|
||||
|
48
utility/tautulli_friendly_name_to_ombi_alias_sync.py
Normal file
48
utility/tautulli_friendly_name_to_ombi_alias_sync.py
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Sync Tautulli friendly names with Ombi aliases (Tautulli as master).
|
||||
|
||||
Author: DirtyCajunRice
|
||||
Requires: requests, python3.6+
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from requests import Session
|
||||
from plexapi.server import CONFIG
|
||||
from urllib3 import disable_warnings
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
OMBI_BASEURL = ''
|
||||
OMBI_APIKEY = ''
|
||||
|
||||
TAUTULLI_BASEURL = ''
|
||||
TAUTULLI_APIKEY = ''
|
||||
|
||||
# Dont Edit Below #
|
||||
TAUTULLI_BASEURL = TAUTULLI_BASEURL or CONFIG.data['auth'].get('tautulli_baseurl')
|
||||
TAUTULLI_APIKEY = TAUTULLI_APIKEY or CONFIG.data['auth'].get('tautulli_apikey')
|
||||
OMBI_BASEURL = OMBI_BASEURL or CONFIG.data['auth'].get('ombi_baseurl')
|
||||
OMBI_APIKEY = OMBI_APIKEY or CONFIG.data['auth'].get('ombi_apikey')
|
||||
|
||||
disable_warnings(InsecureRequestWarning)
|
||||
SESSION = Session()
|
||||
SESSION.verify = False
|
||||
|
||||
HEADERS = {'apiKey': OMBI_APIKEY}
|
||||
PARAMS = {'apikey': TAUTULLI_APIKEY, 'cmd': 'get_users'}
|
||||
|
||||
TAUTULLI_USERS = SESSION.get('{}/api/v2'.format(TAUTULLI_BASEURL.rstrip('/')), params=PARAMS).json()['response']['data']
|
||||
TAUTULLI_MAPPED = {user['username']: user['friendly_name'] for user in TAUTULLI_USERS
|
||||
if user['user_id'] != 0 and user['friendly_name']}
|
||||
OMBI_USERS = SESSION.get('{}/api/v1/Identity/Users'.format(OMBI_BASEURL.rstrip('/')), headers=HEADERS).json()
|
||||
|
||||
for user in OMBI_USERS:
|
||||
if user['userName'] in TAUTULLI_MAPPED and user['alias'] != TAUTULLI_MAPPED[user['userName']]:
|
||||
print("{}'s alias in Tautulli ({}) is being updated in Ombi from {}".format(
|
||||
user['userName'], TAUTULLI_MAPPED[user['userName']], user['alias'] or 'empty'
|
||||
))
|
||||
user['alias'] = TAUTULLI_MAPPED[user['userName']]
|
||||
put = SESSION.put('{}/api/v1/Identity'.format(OMBI_BASEURL.rstrip('/')), json=user, headers=HEADERS)
|
||||
if put.status_code != 200:
|
||||
print('Error updating {}'.format(user['userName']))
|
Reference in New Issue
Block a user