490 Commits

Author SHA1 Message Date
e097a07ca5 Fix for #411 2024-01-06 11:51:14 -05:00
5bcdf4fe43 Merge remote-tracking branch 'origin/master' 2023-11-25 12:33:09 -05:00
5af2e6a7ae Adding feature for #391.
use `--markWatched` to mark the removed item as Watched.
2023-11-25 12:30:49 -05:00
c30850673a Merge pull request #378 from JohnnyGrey86/patch-2
Update README.md
2023-06-08 23:51:59 -04:00
9f4d7db7d2 Merge pull request #365 from JonnyWong16/feature/hide_episode_spoilers_upload
Add upload option to hide_episode_spoilers.py
2023-06-08 23:47:41 -04:00
73bf20d86d Merge pull request #375 from JonnyWong16/feature/merge_multiepisodes
Use `batchEdits()` and add `composite_thumb` option to merge_multiepisodes
2023-06-08 23:47:23 -04:00
a3d3e6c295 Update README.md
Changed URL for Tautulli Custom Notification Conditions wiki article.
2023-03-05 14:16:38 -06:00
02c09389dd Add composite_thumb option to merge_multiepisodes 2023-02-19 14:26:45 -08:00
f28a52d727 Use batchEdits() method for merge_multiepisodes 2023-02-17 11:00:46 -08:00
c52df7c5ee Adding batching method to find all watched content. Fixes #368 2023-01-27 10:44:15 -05:00
9a8cde23bd Updating function names due to deprecation 2023-01-27 10:42:50 -05:00
8e68e8ce7e Merge pull request #370 from JonnyWong16/feature/streaming_service_availability
Add streaming_service_availability.py
2023-01-27 09:26:40 -05:00
078bbccc54 Remove plex_netflix_check.py 2023-01-18 09:47:59 -08:00
b2ceefe975 Fallback to PlexAPI config for PLEX_URL and PLEX_TOKEN 2023-01-18 09:47:35 -08:00
c19b05cb4d Add subtitle to media title printout
* Resolution for movies
* Number of seasons for tv shows
2023-01-18 09:47:35 -08:00
f8a95935f9 Flip libraries conditional 2023-01-17 22:13:16 -08:00
4d162d849d Add streaming_service_availability.py 2023-01-16 22:02:27 -08:00
a04b1a3c32 Add upload option to hide_episode_spoilers.py 2022-12-22 12:37:33 -08:00
10287e3885 Removed old gist posting in favor of just creating the geojson file. 2022-12-13 01:35:39 -05:00
62e7bbc209 Marker size changes 2022-12-13 01:34:53 -05:00
e6637bf3b7 Fixes #348 comment added to code 2022-12-01 11:23:07 -05:00
e065626cdd If no items are unwatched in the input range just exit. 2022-12-01 11:22:27 -05:00
76b2a7274a Changing User Reporting to actually use TIME_DISPLAY variable. 2022-11-28 12:26:40 -05:00
9ae6b9de7c Merge remote-tracking branch 'origin/master' 2022-10-02 16:29:15 -04:00
294a88bd19 friend attribute no longer relevant 2022-10-02 16:25:47 -04:00
501be17842 Merge pull request #344 from origamiofficial/patch-1
Instructions Update
2022-09-19 21:39:35 -04:00
5cb980bf6c Merge pull request #353 from pranscript/feature-utility_media_manager_update
Updated media_manager to add filter based on audience rating
2022-09-19 21:39:08 -04:00
3a2f9d5b12 Added feature filters based on audience rating 2022-08-21 11:50:55 -05:00
569f24ee7c Instructions Update
A simple instruction update to make the script more user friendly.
2022-05-19 13:48:30 +06:00
135d94a283 update docstring removing jsonpickle requirement 2022-05-11 10:31:16 -04:00
c68de7ac66 addressing #336 2022-05-06 13:01:45 -04:00
bef56becde Merge remote-tracking branch 'origin/master' 2022-05-04 14:48:45 -04:00
5a2c50d1be adding examples 2022-05-04 14:48:28 -04:00
37d651605b Merge pull request #262 from blacktwin/exclude_only
Adding Exclude Only restrictions
2022-04-24 12:24:18 -04:00
2ca924e085 smart playlist export/import 2022-04-21 22:35:03 -04:00
39560ce6f5 import work 2022-04-21 14:44:20 -04:00
536da598bd adding json and json file checking for files in script's root. 2022-04-21 14:44:20 -04:00
1d6dece7be add logger 2022-04-21 14:44:20 -04:00
a4dc39e706 more simplifying 2022-04-21 14:44:20 -04:00
111357934e more clean up after removing jsonpickle 2022-04-21 14:44:20 -04:00
ef464a1dbf dropping need for jsonpickle 2022-04-21 14:44:20 -04:00
cfe4182f0a changing method to simplify what is being exported 2022-04-21 14:44:19 -04:00
8ede33d83d Merge pull request #338 from JonnyWong16/feature/add_label
Simplify `add_label_recently_added.py`
2022-04-06 11:58:18 -04:00
3e1af802fa Merge pull request #335 from JonnyWong16/feature/merge_multiepisodes
Fix `merge_multiepisode.py` renumbering episodes across seasons
2022-04-06 11:58:03 -04:00
1f360bd6da Fix merge_multiepisode renumbering across seasons
* Also adds optional `--renumber` argument
2022-03-31 18:47:53 -07:00
cc6e98b7b2 Simplify add_label_recently_added.py using plexapi 2022-03-31 18:47:19 -07:00
a2ec53e4a3 Merge pull request #333 from EVOTk/EVOTk-typo-tautulli
Typo Tautulli - Update README
2022-03-17 13:58:18 -04:00
bfaeed0736 Merge pull request #286 from Palshee/patch-1
Update ips_to_maps.py
2022-03-17 13:57:58 -04:00
0a1e19b18f Merge pull request #329 from JonnyWong16/feature/lock_unlock_poster_art
Faster lock/unlock for posters/art in a library
2022-03-17 13:57:40 -04:00
df09d109a8 Update README.md 2022-02-15 00:30:48 +01:00
46afeff95c Faster lock/unlock with library lockAllField and unlockAllField 2022-02-04 11:58:13 -08:00
31e100835c Merge pull request #322 from JonnyWong16/feature/merge_multiepisodes
Add merge_multiepisodes.py
2022-02-04 14:32:43 -05:00
b2f420b6fb Add note about reverting changes and splitting episodes 2022-01-20 21:45:22 -08:00
07b0c942ab Add episode renumbering 2022-01-20 21:07:40 -08:00
58c8501529 Add merge_multiepisodes.py 2022-01-02 11:27:27 -08:00
39fbaa3ade Merge pull request #317 from JonnyWong16/feature/hide_episode_spoilers
Remove  Tautulli dependency for hide_episode_spoilers
2021-12-06 14:00:51 -05:00
d62a7e9715 Merge pull request #318 from JonnyWong16/feature/lock_unlock_poster_art
Add utility/lock_unlock_poster_art.py
2021-12-06 14:00:33 -05:00
012a9fd4f6 Add utility/lock_unlock_poster_art.py
Script to automatically lock/unlock posters and artwork in a Plex.
2021-11-29 14:40:48 -08:00
a81ff1fdf9 Pass PlexServer object to modify_episode_artwork function 2021-11-23 09:29:00 -08:00
f706e57701 Remove Tautulli dependency for hide_episode_spoilers 2021-11-22 21:38:41 -08:00
bf605ab628 Merge remote-tracking branch 'origin/master' 2021-10-25 00:47:57 -04:00
dbdbf2ad4a fix for sharing from admin 2021-10-25 00:41:10 -04:00
58a962a855 more clean up 2021-10-08 10:26:31 -04:00
0a831fa07e clean up and removed try/exceptions 2021-10-08 10:22:25 -04:00
4305a1190b adding show support 2021-10-08 10:04:04 -04:00
9a16f5ff28 library loop fix ups 2021-10-08 09:23:11 -04:00
0de8004ea0 update variable naming 2021-10-08 09:08:11 -04:00
1399208ca3 refactor for user server account switching
add collection support
2021-10-08 08:52:57 -04:00
fad1c309a7 alignment fix 2021-10-08 08:50:10 -04:00
57afb59f0b Merge pull request #302 from JonnyWong16/feature/hide_episode_spoilers
Add option for summary prefix in hide_episode_spoilers.py
2021-08-26 23:12:48 -04:00
c84cee5fec include check for whether or not the title template returns anything
in the case of returning nothing, script would delete every playlist
Resolves #303
2021-08-17 08:00:45 -04:00
a479596b35 update action should now delete previous In History playlists. 2021-08-04 08:40:38 -04:00
42bd315c1e Make summay_prefix string optional 2021-07-29 13:14:10 +00:00
3204a0cd44 Fix summary prefix example 2021-07-28 18:25:54 -07:00
d076426bf7 Fix line formatting 2021-07-28 18:25:00 -07:00
8c6689796c Add option for summary prefix in hide_episode_spoilers.py 2021-07-28 15:32:45 -07:00
126f14bd3d fix for #299 update positional args change in createPlaylist 2021-07-15 23:21:20 -04:00
4125214b66 keep section_id an int 2021-07-15 23:20:17 -04:00
d10c6cf94f Merge pull request #292 from yosifkit/patch-1
Scope seen, missing, and unique per media type
2021-07-13 09:36:04 -04:00
72fd5c4e51 Merge pull request #298 from JonnyWong16/feature/hide_episode_spoilers
Update hide_episode_spoilers to work with entire show or season
2021-07-13 09:35:00 -04:00
29ab0ebaa4 Update hide_episode_spoilers to work with entire show or season 2021-07-02 16:02:22 -07:00
08f872b35c complete redo
use removeFromContinueWatching action instead of hack job
2021-05-24 11:19:22 -04:00
bf8bd61e17 Updated search urls, results page handling, default site to Netflix
added PlexAPI Config
2021-05-24 10:44:36 -04:00
69fd40e25a filterFields deprecated, using listFields 2021-05-24 10:02:42 -04:00
973adc0148 Scope seen, missing, and unique per media type
Also drop unused "dupes" list
2021-05-08 18:15:37 -07:00
02a8831fb5 reordering graphs 2021-04-15 00:39:41 -04:00
57b91ff139 draw new graph each library
fixes x-axis from slipping and previous stats overlay
2021-04-15 00:38:42 -04:00
4a67ece43d Merge pull request #270 from blacktwin/last_played
Updating media_manager
2021-03-22 10:41:19 -04:00
e55c7db5da EOF 2021-03-22 10:40:42 -04:00
f1ed5fb174 Update ips_to_maps.py
leg.draggable() needs to now be leg.set_draggable(state=True) due to update with matplotlib
2021-03-10 10:57:55 -08:00
96831b5990 addressing issue with exporting admin playlists. 2021-02-27 02:05:39 -05:00
40131f5c20 Merge remote-tracking branch 'origin/master' 2021-02-04 00:15:57 -05:00
5dafd7eb93 simplify connect_to_server method
parsing the IP was not needed
2021-02-04 00:15:42 -05:00
a238742b1b addressing #279
--date can now accept YYYY-MM-DD or X days
2021-02-02 01:25:51 -05:00
9af6626b6d Merge pull request #278 from blacktwin/sync_watched_update
Update to search for watched items
2021-01-08 08:12:28 -05:00
0aa5abd6c8 adding check for str and removing AttributeError 2021-01-06 16:12:12 -05:00
592029cee3 additional exception for previously deleted items. 2021-01-06 13:17:21 -05:00
5a41abd974 search update addressing #276 2020-12-29 15:42:37 -05:00
3421661e22 adding try/except for addressing #273 2020-12-21 13:27:59 -05:00
37aab68400 clean up show actions for reminder of selectors 2020-12-20 16:43:38 -05:00
12eb897ad0 indention corrections 2020-12-20 16:41:32 -05:00
0125865124 adding try/except for addressing #253 2020-12-20 16:05:33 -05:00
7f20da0ca9 Correction for when history may pull ratingKeys as None, this causes errors later on. 2020-12-20 15:58:30 -05:00
ce8610d0ed add action_show function to route all action calls of show 2020-12-20 15:57:38 -05:00
06589274bf update the metadata when pulling from history. Get the items metadata instead of just the history attributes 2020-12-20 15:51:18 -05:00
ebfbc51f72 initial work for adding last_played. 2020-12-18 08:37:59 -05:00
3036a126ba Update bug_report.md 2020-12-17 12:00:12 -05:00
639c0395c2 addressing #253 starting work on adding last_played 2020-12-17 08:11:16 -05:00
557aabefb8 addressing #259 2020-12-09 21:02:50 -05:00
4632dfc600 #254 broke for admin playlists checking. No admin or allUsers will work. 2020-12-04 23:17:45 -05:00
8e4e58792d users for arg to match everything else, thanks chuccck 2020-12-04 23:17:25 -05:00
2369390c4d allowing for tvLabels and movieLabels to use EXCLUDE LABELS
update to arg usage
--movieLabels label="This Thing" label=Thing label!="Not Thing"
2020-12-04 23:09:26 -05:00
547e2c45b3 Merge pull request #250 from blacktwin/library_growth
create library_growth script
2020-12-04 00:21:26 -05:00
e54372379b addressing #255
remove unneeded loop
2020-11-25 23:02:52 -05:00
84efcc631f check if get_ratings_lst returns None
will return None if library is empty

Thanks @InevitableSprinkles
2020-11-21 10:48:54 -05:00
8c302e4187 Merge remote-tracking branch 'origin/master' 2020-11-20 09:41:31 -05:00
8a46b7a237 fix for --action delete --ratingKey usage 2020-11-20 09:41:12 -05:00
c24fcc980b Merge pull request #254 from ology/master
Use the playlist.title, not the playlist object, for logging
2020-11-12 09:05:08 -05:00
573651d31b Map the playlist.title, not the playlist object for logging 2020-11-11 12:15:08 -08:00
0ecd6c097f update filters_lst to reflect change in plexapi 2020-11-03 01:07:29 -05:00
65285d4b3a fix for remove action not working on jbop predefined titles 2020-11-03 00:36:16 -05:00
2b776137a4 if else indent correction 2020-11-02 22:50:57 -05:00
fe33f6d9e0 updating examples to include --libraries arg 2020-10-28 16:09:55 -04:00
abff846216 if title is a list create list of titles to check against. 2020-10-26 16:05:00 -04:00
244e8ad71c unicodedata removal. py3 str used to normalize titles 2020-10-26 09:09:09 -04:00
41ed4199e1 Merge remote-tracking branch 'remotes/origin/python3' 2020-10-12 14:15:20 -04:00
26a37f456a Merge pull request #231 from JonnyWong16/python3
Futurize scripts for Python 3
2020-10-12 13:59:07 -04:00
fc2fba2f2b create library_growth script 2020-10-12 13:34:05 -04:00
29b53dbd4b create music_folder_collections.py 2020-10-10 19:31:46 -04:00
e5a1f7fba0 including @sdlynx contribution 2020-09-28 08:09:55 -04:00
5dcc0cb515 fix for showing playlist titles 2020-09-28 08:05:55 -04:00
0107ec6253 Merge pull request #241 from pjft/patch-1
Slight fixes to gmusic to plex scripts
2020-09-28 07:56:25 -04:00
68e53f6f9d Added remaining changes 2020-09-27 08:41:51 +01:00
a1adb47767 Adjust code from feedback
Thanks for the feedback. Once again, first incursion in python - unsure if this is what you expected on the dictionary front.
Also, added the declaration/instancing of GGMUSICLIST to after the authentication part because it'd fail miserably if we weren't authenticated, I imagine?
As for the behavior, I have a free account, all playlists created by myself, mostly uploaded tracks, some purchased. This seemed to be the way it'd work for me. A simple way I got to troubleshoot it was to run

print("Playlist length: {}".format(len(mc.get_shared_playlist_contents(shareToken))))
print("Playlist SOURCE length: {}".format(len(pl['tracks'])))

and in many cases there was a difference there (the SOURCE would always be the same number or larger than the one from get_shared_playlist_contents(shareToken), and the correct number from what I could tell).

Hope this helps, but once again, your mileage may vary. Just wanted to share in case it'd help.
2020-09-15 20:45:01 +01:00
e24114ad7a Slight fixes to gmusic to plex scripts
- adds more robust playlist song retrieval from Google Play Music for when, for some reason, the get_shared_playlist_contents() method didn't return all the playlist elements;
- removes unnecessary encoding of title and album that were preventing Plex searches from finding the right songs.
2020-09-11 20:51:06 +01:00
28879b77d4 create gmusic_playlists_to_plex.py 2020-07-17 14:47:12 -04:00
aa265463ce remove requirement for pandas 2020-07-17 14:13:20 -04:00
29d328742e replace pandas with builtin csv 2020-07-17 14:12:47 -04:00
3e117480e3 update main docstring
add export examples
2020-07-13 19:53:03 -04:00
77a7614730 add object_cleaner method
add export work for json and csv
2020-07-13 19:48:24 -04:00
c9d369cdf4 change the gathering of all playlists
exclusions for playlists updated for change
2020-07-13 19:47:10 -04:00
f5c2958b90 adding export action and arg 2020-07-13 19:45:50 -04:00
928fd3f194 flatten_json, pandas, and jsonpickle will need to be installed for exporting
#219
2020-07-13 19:45:05 -04:00
f105c0a9c8 add admin user to available users 2020-07-10 01:13:46 -04:00
0d1685afed Run futurize --unicode-literals 2020-07-04 13:31:02 -07:00
dc6507ffed Run futurize --stage2 2020-07-04 13:23:47 -07:00
30dbccda77 Run futurize --stage1 2020-07-04 13:08:59 -07:00
61524c4b92 adding support for Plex's allow/deny restrictions 2020-06-15 16:25:55 -04:00
f90497fbd2 #226 add check for user's existing shares. If no shares, print out "user has no shares"
thanks @RXWatcher
2020-06-14 15:55:10 -04:00
5bf7925585 #226 add check for user's existing shares. If no shares, print out "user has no shares"
thanks @RXWatcher
2020-06-14 15:48:20 -04:00
36aa2dd3a3 address issue found when running with python3 2020-06-04 21:20:54 -04:00
34851652c5 sort most transcodes first 2020-05-20 23:46:53 -04:00
361610707b print amount of items found 2020-05-20 23:46:25 -04:00
0e93543c0d add transcoded selection 2020-05-20 14:43:26 -04:00
7b48654c80 var name updates 2020-05-19 18:11:21 -04:00
56e2239c5a update to Description 2020-05-19 18:10:18 -04:00
bf624c6d7b moving comment 2020-05-19 18:09:27 -04:00
f38f1ee7af support for searching through episodes
using Tautulli data for episode gathering
2020-05-19 18:08:22 -04:00
edfa7315a0 add deletion to watched selection 2020-05-12 21:35:46 -04:00
87b82a5947 initial work on size and rating
focusing on size
2020-05-11 01:00:53 -04:00
fe490699f9 Merge remote-tracking branch 'origin/master' 2020-05-10 23:46:29 -04:00
d7e1e909d1 Author name for rich notifications
simplify subject text
2020-05-10 23:44:17 -04:00
9705be3a94 allowing for only library, user, or both stats 2020-05-10 23:40:46 -04:00
41051a826c add user stat choices
make no notification print out results
2020-05-10 23:20:02 -04:00
49a15fa3fa moving vars
moved colors into rich notifcation area
moved end and start into time/date calculation area
2020-05-10 23:18:57 -04:00
ad2120203c move color vars to top
remove "User: " from USER_STAT
2020-05-10 22:30:28 -04:00
a8d8b1b66f adding rich notifications 2020-05-10 22:27:43 -04:00
b95ac52e00 add actionOption and selectValue args 2020-04-29 22:42:56 -04:00
daee5676ed new notification script
top_concurrent_notify.py
2020-04-21 14:53:41 -04:00
034d772ac0 creating defaults for actions 2020-04-13 14:22:03 -04:00
4a7d69e620 Merge pull request #204 from JonnyWong16/tautulli-scripts
Check remove first in hide_episode_spoilers.py
2020-04-13 13:45:57 -04:00
aac5518ddf condense size and rating to value arg 2020-04-09 16:13:32 -04:00
d38ee9c341 update single quotes to double 2020-04-09 16:05:16 -04:00
7dde481636 fix prints under select watched to be similar to unwatched 2020-04-09 15:51:40 -04:00
c160b66e3f add rating and size selectors and args 2020-04-09 15:48:46 -04:00
844e780cd0 todo update 2020-04-09 15:48:22 -04:00
8ad42fb3e7 #210 expand exception to anything
actually report the offending section id
2020-03-16 10:34:55 -04:00
86b471b5d2 add methods to add collections and labels 2020-03-12 15:51:35 -04:00
5f942142f3 a custom name must be provided for labels or collections 2020-03-12 15:50:59 -04:00
0100aa4246 add label and collection to jbop selections 2020-03-12 15:50:34 -04:00
7e47ee66e4 creation of plex_dance script 2020-03-11 16:09:57 -04:00
a5c5ef3710 add total size of whats been found 2020-03-09 11:52:14 -04:00
98f4fe0742 add check for show media type
gather show location and total size from episodes
2020-03-09 11:51:40 -04:00
1073642054 restructure Metadata class to use get 2020-03-09 11:50:37 -04:00
a7286fb811 update output of show unwatched
include file_size
2020-03-08 11:42:26 -04:00
9d5143ab56 add size conversion method 2020-03-08 11:41:19 -04:00
bc3f41e0ef double check file_size in metadata class 2020-03-08 11:40:43 -04:00
ca38dc30a5 potential fix for #208
section id is printed for user to investigate issue
2020-03-03 11:41:04 -05:00
0b6f3c9a6b print out correction after->before 2020-02-26 14:57:52 -05:00
c38138b41b print out correction after->before 2020-02-26 14:56:51 -05:00
a8e3de7aae creation of media manager
to obsolete other watched/unwatched scripts
can currently:
show watched by users from libraries or rating key
show or delete unwatched items that were added before a date from libraries
2020-02-26 14:52:15 -05:00
ad47db6587 adding call to home_stats
fix time calculations
add library refresh
2020-02-14 16:25:04 -05:00
ff0e834b9c Overhaul
reuse classes and methods from kill_stream
still using old methods for calculations
frameworks for using discord or slack from kill_stream
using plexapi CONFIG file for url and apikey
2020-02-13 09:30:57 -05:00
7b0befad71 Corrections for issue #207 2020-02-11 16:29:27 -05:00
71c2208fee Now works with Python 3.7
error found by slayerinokc in Plex Forums
2020-02-08 13:12:30 -05:00
514151e69b correct issue when using ratingKey with Plex to Plex
Issue reported in Plex forums.
2020-02-05 10:08:38 -05:00
22ff42a373 Merge remote-tracking branch 'origin/master' 2020-01-02 23:26:44 -05:00
1a892fe750 changing variable naming
reuse would cause cascading issue when using allUsers.
2020-01-02 23:25:47 -05:00
674095f5ba Update issue templates 2020-01-02 13:45:19 -05:00
6d7285cb51 Check remove first in hide_episode_spoilers.py
The default `blur` made it skip the remove option
2019-12-19 14:09:58 -08:00
837e733ca2 Merge remote-tracking branch 'origin/master' 2019-12-04 12:13:41 -05:00
9f680cf0ef Revert "save a loop searchEpisodes"
This reverts commit 65b424d
2019-12-04 12:13:10 -05:00
8a80b7b3ac Merge pull request #199 from JonnyWong16/master
Remove default subject and body for rich notification
2019-11-25 08:31:28 -05:00
55740a099a Remove default subject and body for rich notification 2019-11-24 22:38:04 -08:00
720dc4b7b4 Merge pull request #196 from JonnyWong16/plex-scripts
Add Plex modifying metadata scripts
2019-10-28 09:28:15 -04:00
0f8cbc77ef Merge pull request #197 from Arcanemagus/print_raw_error_response
Print raw response when JSON fails to parse
2019-10-28 09:28:07 -04:00
78c644a20c Merge pull request #195 from JonnyWong16/tautulli-scripts
Add Tautulli triggered scripts
2019-10-28 09:27:55 -04:00
66ac37b34e Print raw response when JSON fails to parse
If the response to the Tautulli API fails to parse correctly include the 
raw output in the message to make it easier to diagnose when the 
response isn't actually raw JSON from a Tautulli server (e.g. a proxy 
server).
2019-10-27 20:04:36 -07:00
83bdf49148 Add Plex modifying metadata scripts 2019-10-27 15:59:14 -07:00
5f99b37cfe Add Tautulli triggered scripts 2019-10-27 15:58:42 -07:00
645c43708a Merge pull request #194 from blacktwin/limiterr_readme
update examples
2019-10-24 12:57:56 -04:00
f7f0e19394 update examples
include use of `--days` argument for rolling history
2019-10-23 15:07:07 -04:00
bac3e88ad8 loop correction 2019-10-23 14:56:31 -04:00
65b424d3cc save a loop searchEpisodes 2019-10-19 02:09:52 -04:00
c5f29ac39f Merge remote-tracking branch 'origin/master' 2019-10-14 17:33:12 -04:00
b5fd014c17 #172 update limit section for new history collector 2019-10-14 17:25:58 -04:00
82c7704fab starting work on #172
collect history from N days ago
2019-10-12 16:05:17 -04:00
4ac7313ff0 Merge pull request #189 from DirtyCajunRice/master
Add check for already-unshared libraries
2019-10-02 15:45:27 -04:00
cd809ba2f0 Add check for already-unshared libraries
Fixes #188
2019-10-02 13:42:33 -05:00
dce0dcfd23 adding check in user_lst to omit pending invites.
Closing PR #185
2019-09-27 09:38:34 -04:00
1cbdb149f7 Added apsces in the output 2019-09-11 21:29:55 -04:00
be28995340 Error log. likely item is missing metadata 2019-09-08 10:30:21 -04:00
438673df66 #184 fix for sort_by_date returning None 2019-09-07 23:31:21 -04:00
5d8d40d6ee sort_by_date function was "fixed" out of use. Correcting code to use
function correctly.
2019-07-29 11:26:50 -04:00
85f9dac29e adding logging. only errors will make it to the log file. 2019-07-29 11:20:52 -04:00
e73dca7557 move random and limit down 2019-07-24 08:12:55 -04:00
1fcee2d18f rename var for clarity 2019-07-24 08:11:15 -04:00
245d1d61c4 pull ratingKey not object 2019-07-24 08:10:18 -04:00
8cbdf2de55 #179 If duration is used convert to seconds from minutes 2019-07-22 16:03:50 -04:00
11bd4b1e9f last check for userFrom 2019-07-11 11:49:10 -04:00
63a0427d91 #163 fix for removing shared libraries. 2019-07-11 11:47:57 -04:00
852e355a36 #159 obsolete script.
using conditions are a better way to accomplish this does.
2019-06-25 15:17:40 -04:00
541dd56901 #159 obsolete script. Use newsletter. 2019-06-25 15:16:52 -04:00
b9c2ea66f2 extra print removed. 2019-06-25 14:59:28 -04:00
c80682787b clean up 2019-06-25 14:52:36 -04:00
740dc498e2 add syncing rating key from plex to plex 2019-06-25 14:50:48 -04:00
165e851cbc Merge remote-tracking branch 'origin/master'
# Conflicts:
#	notify/notify_added_custom.py
#	notify/notify_on_added.py
#	utility/sync_watch_status.py
2019-06-25 14:40:47 -04:00
f011d53d8c Merge pull request #171 from Arcanemagus/style-cleanup
Massive style cleanup
2019-06-25 14:38:05 -04:00
60772450a0 Merge branch 'master' into style-cleanup 2019-06-25 14:37:47 -04:00
2d2d88f58c check if rating key is episode or movie and is watched. if not, kill script 2019-06-25 10:50:21 -04:00
1a13d98cd2 add sync of rating key from plex server 2019-06-25 10:32:18 -04:00
12a6875af9 clean up 2019-06-25 10:22:16 -04:00
f9e4ce2ff3 #159 obsolete script. Use newsletter. 2019-06-25 09:32:36 -04:00
f37d735e24 #159 obsolete script.
using conditions are a better way to accomplish this does.
2019-06-25 09:30:04 -04:00
d341092675 simplified code 2019-06-24 15:14:15 -04:00
2be0e376f8 Massive style cleanup
Adds a basic `setup.cfg` file with configurations for flake8 and pylama, 
fixes some basic issues with essentially every file including:
* Many, many, whitespace line cleanups
* Several unused variables and imports
* Missing coding and shabang lines
* Minor style fixes to more closely align with PEP8
* Turn `print` into function calls for Python 2/3 compat
* A few minor bugs
  * Things like using an undefined `i` in `stream_limiter_ban_email.py`
2019-06-20 23:55:11 -07:00
ca5f4fb271 example for --libraryShares 2019-06-17 23:28:10 -04:00
caebf46e83 add --libraryShares argument
Shows all shares by library.
2019-06-17 23:25:05 -04:00
8edde2242f Add searching for episodes with multiple filters or searches 2019-06-17 22:23:56 -04:00
672a2abaf4 clean up 2019-06-16 02:52:49 -04:00
0689a760d8 clean tautulli section watched list 2019-06-16 01:36:05 -04:00
9a28bbf9cb clean up 2019-06-16 00:29:40 -04:00
f8c75a3696 bbox for suptitle 2019-06-16 00:26:23 -04:00
2ab33748cd rename var for clarity 2019-06-16 00:19:50 -04:00
1e18a21d26 comment for explode and color 2019-06-16 00:18:54 -04:00
4bca83c77a tautulli like theme 2019-06-16 00:11:04 -04:00
81eac90620 add filename and headleass arguments 2019-06-15 21:53:49 -04:00
65cc36e9a4 padding and clean up 2019-06-15 21:32:42 -04:00
db5c497cbd Merge remote-tracking branch 'origin/master' 2019-06-15 21:32:06 -04:00
d46b6ee316 Add title 2019-06-15 17:41:43 -04:00
56a84f19dd Allow for data pull from Tautulli 2019-06-15 17:13:47 -04:00
f2458c1089 Merge pull request #168 from philosowaffle/issue/167
[167] plex_lifx_color_theme throws when NumColors=1
2019-06-15 09:26:31 -04:00
983d49ea8e [167] plex_lifx_color_theme throws when NumColors=1 2019-06-13 19:12:22 -05:00
1952413639 add pie chart creation 2019-06-08 01:39:25 -04:00
94128a92cb creation of watched_percentages
output users watched percentage by library
2019-06-04 10:08:54 -04:00
5179aa7175 Add check to see if serverTo and serverFrom are the same. If same, then
use ratingKey instead of searching by name.
2019-06-04 09:02:21 -04:00
a7a6a9381d Credit goes to @jonnywong16 2019-05-29 15:06:20 -04:00
5ddd981f2f multiple filter value support for shows 2019-05-28 14:09:09 -04:00
2a2a9f74ff added function handles multiple search/filter values
clean up for movies sections
genre needed special handling
2019-05-28 13:34:53 -04:00
5e161997dc changed output title to use _prettyfilename 2019-05-28 12:39:20 -04:00
47a8c1ecb4 fix for handling show libraries 2019-05-28 11:35:30 -04:00
149ba7f873 example update for new search addition 2019-05-24 10:38:29 -04:00
ac0db8b69f Merge remote-tracking branch 'origin/master' 2019-05-17 14:13:45 -04:00
517ffbc678 add linked headers 2019-05-17 14:11:40 -04:00
b2bfb400a6 #145 keyword search can now accept multiple keyword searches in one keyword search location.
multiple keywords require an attached comma separator.
`--search title=one,the --search summary="man "`
2019-05-10 00:05:55 -04:00
018b4faa93 Merge pull request #158 from DirtyCajunRice/master
create tautulli_friendly_name_to_ombi_alias_sync
2019-05-04 21:49:36 -04:00
ed65e4e948 rename for arc :P 2019-05-02 15:11:25 -05:00
ba5335c1f0 create ombi_to_tautulli_friendly_name_sync 2019-05-02 15:07:46 -05:00
54bdba174c Merge pull request #157 from DirtyCajunRice/master
remove_inactive_users rework
2019-05-02 13:03:39 -04:00
343c45d78e remove_inactive_users rework 2019-05-01 16:09:50 -05:00
4739ae9615 remove unused function. 2019-04-30 10:12:41 -04:00
7db5c68061 #155, adding kill when duration exceeds limit. Kill at limit. 2019-04-30 10:12:39 -04:00
f626a9daa1 #155 add duration example 2019-04-25 16:11:57 -04:00
7128597beb change single to double quotes 2019-04-25 16:11:34 -04:00
3811bfa5fc #155 adding duration into total time check 2019-04-25 15:54:37 -04:00
bcb85978f0 Merge pull request #154 from DirtyCajunRice/master
get_serial_transcoders.py
2019-04-24 10:00:36 -04:00
a8bd260f33 pep8 + desc 2019-04-23 23:33:57 -05:00
6cd1231ec5 move past_days to used var 2019-04-23 23:28:29 -05:00
926e72a685 force py3 in shebang 2019-04-23 22:22:34 -05:00
1ed9a98bdd add get_serial_transcoders.py 2019-04-23 22:22:19 -05:00
a9d2225de0 use config where applicable 2019-04-23 21:18:55 -05:00
2a4c6f9a22 Merge pull request #153 from DirtyCajunRice/master
Omit local and admin id for purge_removed_plex_friends.py
2019-04-23 08:32:38 -04:00
2258b21b2c cleaner 2019-04-22 15:19:43 -05:00
f35c1a23b0 Omit local and admin id 2019-04-22 15:15:28 -05:00
45185d9ad8 Create new script #152 2019-04-16 10:15:23 -04:00
aa4448de15 Allow for username or email address to select account. 2019-03-25 23:47:11 -04:00
ce4b279fe7 BTC address 2019-03-25 23:45:35 -04:00
5b4feb6208 description update 2019-03-18 22:13:29 -04:00
d90d052696 docstring update 2019-03-18 21:08:03 -04:00
599d9970ea comment updates 2019-03-18 21:06:36 -04:00
0321b883eb example for manual sync from Tautulli user
remove pprint import
2019-03-18 12:14:37 -04:00
5ae2e9f6d7 if ratingKey section cleanup. 2019-03-17 09:56:52 -04:00
ef87f88e05 change for checking if episode is watched from fromUser 2019-03-15 16:21:21 -04:00
aee131548b added manual runs for user to user from tautulli ratingKey 2019-03-15 11:59:24 -04:00
8f02098655 clean up, reduce amount of times connecting to servers. 2019-03-15 00:58:46 -04:00
e605979a16 total rewrite while resolving #146.
Now able to read watched status from Tautulli and apply to any user
on any owned server.
2019-03-14 13:07:41 -04:00
80557f4a61 #146 Adding get_history from Tautulli, initial setup 2019-02-26 10:28:51 -05:00
1adcc9e41b #144 Added ability to sync watched status across all owned servers 2019-02-25 00:20:45 -05:00
e45ff63d9b spacing 2019-02-19 10:07:59 -05:00
cbc71bfc1a #144 adding servers as an arg. 2019-02-19 08:32:29 -05:00
ebc5151fbc save a line 2019-02-19 00:10:31 -05:00
c603efb472 #145 multiple filters with multiple searches is now allowed. 2019-02-18 14:24:40 -05:00
e4d0eaf5b1 #145 fix for multiple filters and one search. Multiple search locations not supported yet. 2019-02-18 14:15:40 -05:00
c3fa69ba28 Capitalizing first letters of each filter used and keyword searched 2019-02-17 01:20:18 -05:00
6e0dd60a84 docstring updates 2019-02-09 01:20:41 -05:00
d2b37837d8 #141 fix for remove action 2019-02-09 01:14:21 -05:00
4c014cfc24 #141 separate title creation to a function.
Now update action will allow updating custom names or random selector
2019-02-08 11:59:05 -05:00
88f775e646 From finding in #142 2019-02-07 00:45:33 -05:00
625948b73e #142 compare USER_LST against who watched each episode 2019-02-07 00:29:23 -05:00
0c815e459b #141 update and delete now work with random selector 2019-02-05 13:06:46 -05:00
6c4d0039e2 #141 corrected missing popular titles error 2019-02-05 00:48:35 -05:00
8651dc34dd trim build_playlist 2019-02-03 23:54:55 -05:00
13099bf557 make sure random selector has a limit 2019-02-03 23:08:25 -05:00
769acfff01 #107 Random selector has been added 2019-02-03 16:58:40 -05:00
34f161de22 clean up, correct showing user's current playlists 2019-02-02 09:46:07 -05:00
e667d48f68 todo, var name 2019-02-02 09:23:54 -05:00
c9c57352f4 docstring correction 2019-01-13 12:16:04 -05:00
58e7fd1335 #138 message fix 2019-01-11 18:20:39 -05:00
11715e1e6e Only users with libraries shared can be selected.
Should resolve #125
2019-01-06 09:03:47 -05:00
ca19d3d6b4 #133 --self did not have all of self's playlists
clean up variable
clean up self or no self selection
2019-01-05 20:22:32 -05:00
3d632aa65e show no longer needs allPlaylists to view user's playlist. 2019-01-05 18:10:57 -05:00
b5c7261c3b restruct 2019-01-05 01:10:02 -05:00
8ef3e5fc82 docsting update, clean up 2019-01-05 01:09:59 -05:00
1409209e58 #133 only include users who have active shares 2019-01-05 01:09:56 -05:00
70accd1767 exclusions docstring 2019-01-04 13:49:05 -05:00
fcdb0dbc3d print update for adding 2019-01-04 13:44:11 -05:00
f3389a1c22 check for libraries when they are needed
fix update function
2019-01-04 13:41:44 -05:00
b5b77963fe fix for playlist exclusions 2019-01-04 13:30:07 -05:00
bcec98f189 allowing --allPlaylists to function properly with remove 2019-01-04 13:21:34 -05:00
c1865f77de create exclusions function 2019-01-04 13:09:43 -05:00
7818058f16 finish playlist exclusion 2019-01-04 10:31:00 -05:00
2cb51cc312 #133 fix for actions with --playlists arg not working properly
removed --playlist choice to allow for selecting any user's playlists
2019-01-04 10:03:10 -05:00
91d7195143 #133 jbop titles were getting capitalized during creation and causing a
mismatch when removing/updating.
2019-01-03 15:56:38 -05:00
bc9d1bc550 Unique IP example update to User Concurrent Streams 2019-01-03 11:41:59 -05:00
63298cbb63 added --allPlaylists
clean up
2019-01-03 11:14:32 -05:00
773e09d730 Merge remote-tracking branch 'origin/master' 2019-01-03 10:40:28 -05:00
83990a7620 #131 now popularTV/Movies can use libraries to select specific libraries 2019-01-03 10:39:55 -05:00
472e438967 Merge pull request #130 from DirtyCajunRice/master
Lots of Updates
2019-01-02 21:39:39 -05:00
f139b19d35 fix for limit check causing keys_lst to be blank. 2019-01-02 12:48:56 -05:00
baa9ff80fe allowing for multiple media type playlist 2019-01-01 09:43:30 -05:00
3b7d5216aa add input filter check
add basic limit and custom naming
2019-01-01 09:26:50 -05:00
bb674a413d allow for custom from multiple media types 2019-01-01 09:03:13 -05:00
d3e6d85290 Shows media type working 2019-01-01 02:46:48 -05:00
f36f7a7ec0 split search and filtering 2018-12-31 23:38:30 -05:00
d9e383f871 removed multipstreams and changed readme to show example of working multi ip stream change 2018-12-23 21:07:46 -06:00
0afd3bf40b updating filters 2018-12-23 02:03:01 -05:00
75665d50a2 #131 --allLibraries added
examples updated
2018-12-23 02:02:22 -05:00
6fcf430152 splitting up search to search and filters. 2018-12-21 21:17:08 -05:00
99eee6fdfb temp fix 2018-12-21 16:24:32 -05:00
640a1f70dc fix for shows 2018-12-21 16:14:52 -05:00
bfdf94fc14 added keyword searching for unfiltered metadata
added filtered searching
2018-12-21 16:05:17 -05:00
29566aa47a move creation of playlist content to build_playlist function. 2018-12-21 09:41:10 -05:00
8fc1e7dbfb update examples 2018-12-21 09:13:06 -05:00
4bb37d0073 Starting to add features from request #101
--jbop historyToday
--jbop historyWeek
--jbop historyMonth
2018-12-21 09:11:05 -05:00
aff964522b clean up 2018-12-21 00:14:11 -05:00
a27056a11f fixed condition in which if user streams is set to more than one, it
would try to kill and notify x times more
2018-12-19 23:49:20 -06:00
79088cf6ce added readme and snuck my name in >.< 2018-12-19 23:47:08 -06:00
93ddee995e fixed rich message pointer from Arc 2018-12-19 23:26:35 -06:00
d1443d2670 final addition to initialize ip_address for linting 2018-12-19 21:18:34 -06:00
758af21ade spacing 2018-12-19 21:17:44 -06:00
457486924e added functionality for dropping multi IP users 2018-12-19 21:15:45 -06:00
f9ce8eae63 removed all instances of shadow of outer scope, made explicit from
implicit inheritance, defined a few docstring params, initialized 2 vars
2018-12-19 19:57:30 -06:00
463586ff29 fixed typos and sams rename 2018-12-19 19:08:27 -06:00
b5347e3304 mass guest access modification script 2018-12-19 18:43:30 -06:00
6d418e54f7 rework logic, add more informative output, and update. 2018-12-19 18:21:54 -06:00
aa6017374e moved to bullet points for uniformity with conditions 2018-12-19 18:02:31 -06:00
72fbf36377 fixed disappearing whitespace 2018-12-19 17:59:21 -06:00
64f2312248 Updated readme to include Transcode Decision Change and explain
transcoding decision vs. video decision
2018-12-19 17:53:29 -06:00
e222525d14 if con_watch is defined by viewOffset then so should On Deck. 2018-12-18 10:27:02 -05:00
dd7228904b First update to address #126
New examples for new arguments
Now able to display user's deck and continue watching
Now able to clear only shows that are marked continue watching
2018-12-17 15:45:26 -05:00
316616f02a spelling fix. 2018-12-07 12:44:59 -05:00
baa687355f #125 removing else 2018-12-07 12:08:56 -05:00
e2475fae3b #125 forgot to use continue 2018-12-07 12:03:10 -05:00
2a39697acd #125 add in checks for when Tautulli friendly name has been edited.
if edited then fallback to use userID to match Tautulli to Plex.
Check if user exists in Tautulli but not Plex
2018-12-07 11:03:33 -05:00
542f58b510 correct exception message 2018-12-07 10:13:58 -05:00
473808d720 #125 fix plex and tautulli url/token calls from CONFIG 2018-12-07 09:42:23 -05:00
f7a7125783 24 is not valid. 0 instead. 2018-12-06 10:16:03 -05:00
f6faa5fe1a correct link for playlist_manager 2018-12-05 20:29:49 -05:00
2e85fdc72e #123 make required libraries for maps scripts separate. 2018-12-03 13:22:32 -05:00
63ef06e814 #123 removing maps section requirements from main. 2018-12-03 13:21:59 -05:00
a162cec74d shebang 2018-11-25 08:44:56 -05:00
534dfe4aeb fix selection to title mistake 2018-11-23 15:45:21 -05:00
92c96ae59d #120 2018-11-23 15:35:21 -05:00
591c299af6 fix for title and transcode sessions 2018-11-19 23:41:58 -05:00
27db3a6eaa metadata api fix 2018-11-18 16:17:31 -05:00
b5c40b29d2 allow user arg for backup 2018-11-09 01:33:47 -05:00
0a445d1ab3 if pending invite user is created but without certain attributes.
filter out by title
2018-11-09 01:33:21 -05:00
7cdb1355f5 spelling 2018-11-08 23:13:41 -05:00
eeed9e6f81 import os missing for deletion 2018-11-05 15:04:08 -05:00
11c5bb9ead selector name changes and adding future types.
selector and title
2018-11-04 01:34:35 -04:00
29ad54f844 select playlist by entire name for deletion 2018-11-03 09:37:33 -04:00
0ce14ada68 fetchItem fix for permissions and deleted/moved rating_key. 2018-11-02 10:29:19 -04:00
a7cf9c784b adding year and week for future sorting
fetchItem error comment
2018-10-30 00:02:19 -04:00
17baec0e5d edit example to include --today 2018-10-24 00:44:11 -04:00
dbb03e595f docstring --today 2018-10-24 00:43:39 -04:00
69f88ef738 fix for shared users who no longer shared libraries on the server. 2018-10-19 03:28:09 -04:00
84c843f54f docsting examples 2018-10-19 02:31:55 -04:00
6a746a6700 potential fix for users without permission to item 2018-10-19 02:01:29 -04:00
a0a634fb3e actions to function for __doc__ in arg help
arg help clean up
2018-10-19 01:32:00 -04:00
a3c3a145f1 update show action docstring 2018-10-19 01:15:37 -04:00
797a445679 add share_playlist to user playlists from admin to users
fixed showing jbop type contents
2018-10-19 01:14:24 -04:00
8087742f33 actions docsting 2018-10-19 00:41:39 -04:00
cda132b2d9 show_playlist to display what the playlist would be before creating 2018-10-19 00:32:55 -04:00
d32f36f51e adding allUsers 2018-10-18 23:57:42 -04:00
8dbd187ef7 Add --action show to display user's current playlist 2018-10-18 21:38:08 -04:00
14ea3bb0d5 --playlist looks for shows in playlist. 2018-10-18 12:30:08 -04:00
f9f8cf401a added --today to only search for history of today 2018-10-16 15:03:48 -04:00
222ce6cd60 remove search_file.py 2018-10-16 14:59:42 -04:00
1e127c6940 correct limiting scripts. 2018-10-16 14:58:23 -04:00
f41501e77c pull show title from Playlists 2018-10-16 09:38:15 -04:00
08e68b896d #97 Enable API instructions. @Daedilus 2018-10-10 08:23:55 -04:00
2a8f18405f add poster thumbs 2018-10-08 00:10:14 -04:00
c1f68df4f5 if rating is None then 0.0 2018-10-08 00:00:26 -04:00
02cc9878a6 fix org_diff function 2018-10-07 23:59:33 -04:00
19e85e6dbb Merge remote-tracking branch 'origin/master' 2018-10-07 09:20:44 -04:00
3f2e3798ad remove admin account name from user_lst
admin playlists handled by --self
2018-10-07 09:16:30 -04:00
bcc7a358a7 update examples 2018-10-07 09:15:20 -04:00
18bd6d8461 remove requirement of users 2018-10-07 09:10:05 -04:00
15a262339d delete aired_today_playlist. Obsolete due to playlist_manager.py. 2018-10-03 14:46:08 -04:00
24360642f7 Merge pull request #96 from Arcanemagus/patch-1
Cleanup Rich Notifications instructions
2018-10-03 14:42:15 -04:00
c96596f422 Cleanup Rich Notifications instructions
Essentially just some stylistic changes, `slack` -> `Slack`, fixing bullet lists, and making the required arguments section clearer.
2018-10-03 11:27:09 -07:00
78d196c4df remove unshare function 2018-09-26 16:08:29 -04:00
85ada26e3d example for limit of specific library 2018-09-26 15:44:10 -04:00
c34736f124 print reason for limit kill 2018-09-26 15:22:11 -04:00
69d1c7f8eb remove plexapi_search_file 2018-09-26 09:12:05 -04:00
46c88b9baf create off_deck 2018-09-26 09:11:20 -04:00
5d28f595e4 create playlist manager 2018-09-26 09:10:40 -04:00
0c2d97cad4 docstring correction 2018-09-26 09:09:46 -04:00
6fff1c61df update rich notification section 2018-09-26 09:09:16 -04:00
467ae80e02 buy me a beer/coffee 2018-09-13 09:49:58 -04:00
e31160d634 Merge remote-tracking branch 'origin/master' 2018-09-11 22:47:30 -04:00
160277dd62 Merge pull request #94 from samwiseg00/feature/rich_notifications
Add rich notifications to kill_stream.py
2018-09-11 22:39:06 -04:00
8fffc30c77 Update README to reflect new kill_stream features 2018-09-08 23:58:08 -04:00
713f6fbe56 Rewrite Kill_stream.py to add rich message functionality 2018-09-08 20:31:06 -04:00
0987874f44 if adding url and token don't check the config.ini 2018-09-08 16:57:30 -04:00
1a9fdd1a99 Issue #93, clean up 2018-09-07 00:32:58 -04:00
286c65f9a2 SSL 2018-08-23 06:44:26 -04:00
5b58e94ab8 shebang 2018-08-23 06:43:58 -04:00
0c0ee06125 shebang and spacing 2018-08-23 06:43:34 -04:00
ce8f3838c0 CONFIG and SSL 2018-08-23 06:41:49 -04:00
9f45bed626 shebang 2018-08-23 06:38:59 -04:00
c3063a77e4 CONFIG and SSL 2018-08-23 06:38:07 -04:00
d802c334fe shebang 2018-08-23 06:37:24 -04:00
d20ecda1ee shebang and remove dup vars 2018-08-23 06:36:35 -04:00
6f30b44431 CONFIG and SSL 2018-08-23 06:34:22 -04:00
18526cf765 shebang 2018-08-23 06:33:24 -04:00
8d7f0fc11b CONFIG and SSL 2018-08-23 06:32:52 -04:00
faf959fa4a shebang 2018-08-23 06:32:06 -04:00
5ea6211956 spacing and shebang 2018-08-23 06:31:17 -04:00
59675d4a5b SSL 2018-08-23 06:30:24 -04:00
1635da7619 shebang 2018-08-23 06:29:54 -04:00
793092ddc8 CONFIG and SSL 2018-08-23 06:29:13 -04:00
8caff8cdc0 shebang 2018-08-23 06:27:13 -04:00
a78244df75 CONFIG and SSL 2018-08-23 06:26:30 -04:00
4782bd9e5e shebang 2018-08-23 06:24:44 -04:00
c5778c89a3 CONFIG and SSL 2018-08-23 06:23:22 -04:00
e39c35ab87 shbang 2018-08-23 06:22:00 -04:00
a336c3aded shebang 2018-08-23 06:20:50 -04:00
ea347159d6 shebang 2018-08-23 06:19:24 -04:00
d5053cdca8 docstring and example update 2018-08-22 21:08:29 -04:00
52d42ab46c pretty up output 2018-08-22 21:05:27 -04:00
b2bcce67ef arg fix and replaced 2018-08-22 20:47:00 -04:00
6f40d31f9d SSL and session 2018-08-22 20:23:32 -04:00
dd33595b98 CONFIG 2018-08-22 20:22:40 -04:00
e9a085c28f update feature updates 2018-08-18 09:49:05 -04:00
038c17c41d Merge pull request #88 from JonnyWong16/kill-pause-username
Add username argument for kill paused
2018-08-14 13:53:16 -04:00
ec07cebef9 Add username argument for kill paused 2018-08-13 21:15:31 -07:00
c474182b97 change function name 2018-08-13 21:33:50 -04:00
9e35fbe091 use CONFIG 2018-08-13 21:32:06 -04:00
587cf32087 prefix backup with servername
restore arg choices startwith servername
2018-08-13 08:20:18 -04:00
970535057d Doc string update description and examples
print settings enabled --add and disabled --remove
fixed issue where filters would be wiped if --add --remove were used
2018-08-13 07:57:30 -04:00
8c3781728a sessionId check bypass if jbop == allstreams.
sessionId arg not needed for killing all user streams.
2018-08-13 07:03:37 -04:00
ffa97daeff sessionId no longer required.
check for sessionId is performed later.
2018-08-13 07:01:05 -04:00
96adf71620 removing server loop from restore. 2018-08-13 06:54:59 -04:00
c3978ace6b remove servers from backup and shared json
attempt to pretty print settings when toggled
add/remove settings independently from libraries
2018-08-12 21:46:38 -04:00
52b91ee9c6 fix for multiple servers. Script will only modify server based on URL and token used. But will still show owned servers and user shares from all owned servers. 2018-08-12 09:18:48 -04:00
66d7cd3aa5 use previously set requests.Session() 2018-08-11 22:21:16 -04:00
124e324c95 spelling and docstring corrections 2018-08-08 11:59:35 -04:00
f25a881a27 fixes for non Plex Pass members. 2018-08-08 11:50:14 -04:00
3cd993aaad miss spelled Tautulli. 2018-08-08 06:22:36 -04:00
80 changed files with 7220 additions and 2059 deletions

42
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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.

View File

@ -17,14 +17,14 @@ print(plexapi.CONFIG_PATH)
[![PM](https://img.shields.io/badge/Discord-Scripts-lightgrey.svg?colorB=7289da)](https://discord.gg/tQcWEUp) [![PM](https://img.shields.io/badge/Reddit-Message-lightgrey.svg)](https://www.reddit.com/user/Blacktwin/) [![PM](https://img.shields.io/badge/Plex-Message-orange.svg)](https://forums.plex.tv/u/blacktwin) [![Issue](https://img.shields.io/badge/Submit-Issue-red.svg)](https://github.com/blacktwin/JBOPS/issues/new)
### Donation
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](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> &nbsp;&nbsp; or &nbsp;&nbsp; <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>

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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__':

View File

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

View File

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

View File

@ -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."
```

View File

@ -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` \]

View File

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

View File

@ -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
View File

@ -0,0 +1,8 @@
#---------------------------------------------------------
# Potential requirements.
# pip install -r requirements.txt
#---------------------------------------------------------
requests
matplotlib
numpy
basemap

View File

@ -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 = """\

View File

@ -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'])

View File

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

View File

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

View File

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

View File

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

View 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.")

View File

@ -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')

View File

@ -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:

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

View File

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

View File

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

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

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

View File

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

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

View File

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

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

View File

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

View File

@ -4,6 +4,4 @@
#---------------------------------------------------------
requests
plexapi
matplotlib
numpy
basemap
urllib3

View File

@ -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
View 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

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

View File

@ -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)')

View File

@ -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'])

View 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]}')

View File

@ -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.')

View File

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

View 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}%")

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

View File

@ -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):

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

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -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'])

View File

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

View File

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

View File

@ -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]

View File

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

View File

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

View File

@ -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

View File

@ -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')

View 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')))

View File

@ -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

View File

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

View 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])

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

@ -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!")

View 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']))