plexdo 1.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. plexdo/__init__.py +26 -0
  2. plexdo/__main__.py +24 -0
  3. plexdo/accounts.py +253 -0
  4. plexdo/airdates.py +136 -0
  5. plexdo/cache.py +38 -0
  6. plexdo/cli.py +157 -0
  7. plexdo/commands/__init__.py +63 -0
  8. plexdo/commands/auth.py +201 -0
  9. plexdo/commands/build.py +194 -0
  10. plexdo/commands/copy.py +169 -0
  11. plexdo/commands/libraries.py +231 -0
  12. plexdo/commands/metadata.py +135 -0
  13. plexdo/commands/missing.py +234 -0
  14. plexdo/commands/playlists.py +174 -0
  15. plexdo/commands/rescan.py +95 -0
  16. plexdo/commands/search.py +114 -0
  17. plexdo/commands/status.py +245 -0
  18. plexdo/commands/stream.py +138 -0
  19. plexdo/commands/users.py +39 -0
  20. plexdo/commands/watched.py +295 -0
  21. plexdo/config.py +153 -0
  22. plexdo/console.py +240 -0
  23. plexdo/constants.py +111 -0
  24. plexdo/convert.py +38 -0
  25. plexdo/data/_plexdo +359 -0
  26. plexdo/data/plexdo.1 +571 -0
  27. plexdo/data/plexdo.bash +597 -0
  28. plexdo/data/plexdo.fish +317 -0
  29. plexdo/data/plexdo.ps1 +371 -0
  30. plexdo/formats.py +181 -0
  31. plexdo/gallery.py +190 -0
  32. plexdo/identify.py +75 -0
  33. plexdo/logs.py +21 -0
  34. plexdo/m3u.py +34 -0
  35. plexdo/paths.py +105 -0
  36. plexdo/photos.py +74 -0
  37. plexdo/playlists.py +174 -0
  38. plexdo/py.typed +0 -0
  39. plexdo/records.py +136 -0
  40. plexdo/sections.py +58 -0
  41. plexdo/security.py +65 -0
  42. plexdo/sorting.py +50 -0
  43. plexdo/throttle.py +59 -0
  44. plexdo/titles.py +53 -0
  45. plexdo/tokens.py +88 -0
  46. plexdo-1.1.18.dist-info/METADATA +601 -0
  47. plexdo-1.1.18.dist-info/RECORD +51 -0
  48. plexdo-1.1.18.dist-info/WHEEL +5 -0
  49. plexdo-1.1.18.dist-info/entry_points.txt +2 -0
  50. plexdo-1.1.18.dist-info/licenses/LICENSE +232 -0
  51. plexdo-1.1.18.dist-info/top_level.txt +1 -0
plexdo/data/_plexdo ADDED
@@ -0,0 +1,359 @@
1
+ #compdef plexdo
2
+ # zsh completion for plexdo
3
+ #
4
+ # Installation:
5
+ # Per user: cp _plexdo ~/.local/share/zsh/site-functions/
6
+ # (ensure that directory is in $fpath, then: compinit)
7
+ # System-wide: cp _plexdo /usr/local/share/zsh/site-functions/
8
+ #
9
+ # Completion for user IDs, library IDs, rating keys, playlists, and albums is
10
+ # read from the 15-minute cache under ~/.cache/plexdo that the list commands
11
+ # populate. A stale cache is refreshed in the background so completion never
12
+ # blocks; the values shown come from whatever is cached at that moment.
13
+
14
+ # Some systems ship "python" but not "python3".
15
+ _plexdo_python() {
16
+ if (( $+commands[python3] )); then print -r -- python3; else print -r -- python; fi
17
+ }
18
+
19
+ (( $+functions[_plexdo_cache_dir] )) || _plexdo_cache_dir() {
20
+ # Memoised: this reads the config file to honour [plex] cache_dir, so it
21
+ # should not run once per completion candidate.
22
+ if [[ -z $_plexdo_cache_resolved ]]; then
23
+ typeset -g _plexdo_cache_resolved
24
+ _plexdo_cache_resolved="$("$(_plexdo_python)" - <<'PYCACHE' 2>/dev/null
25
+ import configparser, os
26
+ from pathlib import Path
27
+ if os.name == "nt":
28
+ base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
29
+ app = Path(base) / "PlexDo" if base else Path.home() / "AppData/Local/PlexDo"
30
+ cfg_path, default = app / "plexdo.ini", app / "Cache"
31
+ else:
32
+ cfg_path = Path("~/.local/etc/plexdo.ini").expanduser()
33
+ default = Path("~/.cache/plexdo").expanduser()
34
+ value = ""
35
+ if cfg_path.exists():
36
+ parser = configparser.ConfigParser(interpolation=None)
37
+ try:
38
+ parser.read(cfg_path, encoding="utf-8")
39
+ value = (parser.get("plex", "cache_dir", fallback="") or "").strip()
40
+ except Exception:
41
+ value = ""
42
+ print(Path(os.path.expandvars(value)).expanduser() if value else default)
43
+ PYCACHE
44
+ )"
45
+ fi
46
+ print -r -- "$_plexdo_cache_resolved"
47
+ }
48
+
49
+ # Return 0 when the cache file exists and is under 15 minutes old.
50
+ _plexdo_cache_fresh() {
51
+ local file=$1 now mtime
52
+ [[ -f $file ]] || return 1
53
+ now=$(date +%s)
54
+ mtime=$(stat -c %Y "$file" 2>/dev/null) || mtime=$(stat -f %m "$file" 2>/dev/null) || return 1
55
+ (( now - mtime < 900 ))
56
+ }
57
+
58
+ # Run a plexdo list command in the background purely for its cache side effect.
59
+ _plexdo_refresh() {
60
+ local prog=${words[1]:-plexdo}
61
+ ( "$prog" "$@" >/dev/null 2>&1 & ) 2>/dev/null
62
+ }
63
+
64
+ # Emit "value" or "value:description" lines from a JSON cache file.
65
+ _plexdo_read_cache() {
66
+ local file=$1 field=$2 desc=$3
67
+ [[ -f $file ]] || return
68
+ python3 - "$file" "$field" "$desc" <<'PY' 2>/dev/null
69
+ import json, sys
70
+ path, field, desc = sys.argv[1], sys.argv[2], sys.argv[3]
71
+ try:
72
+ rows = json.load(open(path, encoding="utf-8"))
73
+ except Exception:
74
+ sys.exit(0)
75
+ for row in rows:
76
+ value = str(row.get(field, "")).strip()
77
+ if not value:
78
+ continue
79
+ if desc:
80
+ label = str(row.get(desc, "")).strip().replace(":", " ")
81
+ print(f"{value}:{label}" if label else value)
82
+ else:
83
+ print(value)
84
+ PY
85
+ }
86
+
87
+ _plexdo_users() {
88
+ local cache="$(_plexdo_cache_dir)/users.json"
89
+ _plexdo_cache_fresh $cache || _plexdo_refresh list-users
90
+ local -a ids titles
91
+ ids=( '0:admin account' ${(f)"$(_plexdo_read_cache $cache id title)"} )
92
+ titles=( ${(f)"$(_plexdo_read_cache $cache title '')"} )
93
+ _describe -t user-ids 'user id' ids
94
+ [[ -n $titles ]] && _describe -t user-titles 'user title' titles
95
+ }
96
+
97
+ _plexdo_libraries() {
98
+ local cache="$(_plexdo_cache_dir)/libraries.json"
99
+ _plexdo_cache_fresh $cache || _plexdo_refresh list-libraries
100
+ local -a libs titles
101
+ libs=( ${(f)"$(_plexdo_read_cache $cache id title)"} )
102
+ titles=( ${(f)"$(_plexdo_read_cache $cache title '')"} )
103
+ _describe -t library-ids 'library id' libs
104
+ [[ -n $titles ]] && _describe -t library-titles 'library title' titles
105
+ }
106
+
107
+ _plexdo_rating_keys() {
108
+ local dir="$(_plexdo_cache_dir)" file lib
109
+ local -a keys
110
+ for file in $dir/titles.*.json(N); do
111
+ if ! _plexdo_cache_fresh $file; then
112
+ lib=${${file:t}#titles.}; lib=${lib%.json}
113
+ _plexdo_refresh list-titles $lib
114
+ fi
115
+ keys+=( ${(f)"$(_plexdo_read_cache $file ratingKey title)"} )
116
+ done
117
+ if (( ! ${#keys} )); then
118
+ _plexdo_refresh list-libraries
119
+ _message 'rating key (run list-titles <library_id> to populate completion)'
120
+ return
121
+ fi
122
+ _describe -t rating-keys 'rating key' keys
123
+ }
124
+
125
+ # $1 is the user id or title already typed on the command line.
126
+ _plexdo_playlists() {
127
+ local user=${1:-0} dir="$(_plexdo_cache_dir)"
128
+ local cache="$dir/playlists.${user}.json"
129
+ _plexdo_cache_fresh $cache || _plexdo_refresh list-playlists $user
130
+ local -a names
131
+ names=( ${(f)"$(_plexdo_read_cache $cache title '')"} )
132
+ if (( ${#names} )); then
133
+ _describe -t playlists 'playlist' names
134
+ else
135
+ _message 'playlist title'
136
+ fi
137
+ }
138
+
139
+ # Playlists by title or ratingKey, for list-playlist which accepts either.
140
+ _plexdo_playlists_or_keys() {
141
+ local user=${1:-0} cache="$(_plexdo_cache_dir)/playlists.${user}.json"
142
+ _plexdo_cache_fresh $cache || _plexdo_refresh list-playlists $user
143
+ local -a names keys
144
+ names=( ${(f)"$(_plexdo_read_cache $cache title '')"} )
145
+ keys=( ${(f)"$(_plexdo_read_cache $cache ratingKey title)"} )
146
+ [[ -n $names ]] && _describe -t playlists 'playlist' names
147
+ [[ -n $keys ]] && _describe -t playlist-keys 'playlist ratingKey' keys
148
+ }
149
+
150
+ # Album names, derived from the "Album - Photo" titles cached for a library.
151
+ _plexdo_albums() {
152
+ local lib=${1:-} cache="$(_plexdo_cache_dir)/titles.${lib}.json"
153
+ [[ -z $lib ]] && { _message 'album name'; return }
154
+ _plexdo_cache_fresh $cache || _plexdo_refresh list-titles $lib
155
+ local -a albums
156
+ albums=( ${(fu)"$(_plexdo_read_cache $cache title '' | sed -n 's/ - .*//p')"} )
157
+ if (( ${#albums} )); then
158
+ _describe -t albums 'album' albums
159
+ else
160
+ _message 'album name'
161
+ fi
162
+ }
163
+
164
+ # Nth (0-based) positional argument after the subcommand, "" when absent.
165
+ _plexdo_positional() {
166
+ local want=$1 i=2 skip=0 count=0
167
+ for (( i = 2; i <= $#words - 1; i++ )); do
168
+ if (( skip )); then skip=0; continue; fi
169
+ case ${words[i]} in
170
+ --m3u|--album|--sort|--media-type|--library-id|-l|--library|-t|--title|\
171
+ -u|--username|-p|--password|-c|--code|-f|--format|--section|-p|--prefix|--throttle) skip=1; continue ;;
172
+ -*) continue ;;
173
+ esac
174
+ if (( count == want )); then print -r -- ${words[i]}; return; fi
175
+ (( count++ ))
176
+ done
177
+ }
178
+
179
+ _plexdo_commands() {
180
+ local -a cmds
181
+ cmds=(
182
+ 'list-libraries:List all Plex libraries'
183
+ 'list-titles:List titles in a library'
184
+ 'list-library:List titles in a library (alias for list-titles)'
185
+ 'list-show:List all episodes in a show'
186
+ 'export-titles:Export an entire library to M3U or an HTML gallery'
187
+ 'search:Search Plex for titles matching a query'
188
+ 'list-users:List all managed/home users'
189
+ 'list-playlists:List playlists for a user'
190
+ 'list-playlist:List items in a specific playlist'
191
+ 'export-playlist:Export an existing playlist to an M3U file'
192
+ 'remove-playlist:Delete a playlist from a user'
193
+ 'append-playlist:Append items to an existing playlist'
194
+ 'show-metadata:Display metadata for a single item'
195
+ 'read:Stream a media file to stdout'
196
+ 'rescan:Trigger a library rescan or show scan status'
197
+ 'status:Show server identity, sessions, users, scans, and tasks'
198
+ 'find-missing:Find gaps in episode numbering across every show'
199
+ 'build-interleaved:Round-robin interleaved playlist from shows'
200
+ 'build-chronological:Date-sorted playlist from shows and movies'
201
+ 'build-randomize:Randomize a playlist into a new one'
202
+ 'copy-playlist-all-users:Copy a playlist to all managed users'
203
+ 'copy-playlist-to-user:Copy a playlist to a specific user'
204
+ 'copy-watched:Synchronise watched state between two users'
205
+ 'login:Authenticate with plex.tv and save a token'
206
+ 'write-config-example:Write a template config file'
207
+ )
208
+ _describe -t commands 'plexdo command' cmds
209
+ }
210
+
211
+ _plexdo_global_opts=(
212
+ '--json[Output machine-readable JSON instead of tables]'
213
+ '--verbose[Print high-level progress to stderr]'
214
+ '--debug[Print detailed internal logs to stderr]'
215
+ '--dry-run[Show what would happen without mutating Plex]'
216
+ '(-W --wide)'{-W,--wide}'[Do not shrink table columns to the terminal width]'
217
+ '--throttle[Seconds between requests in per-item operations; 0 disables]:seconds:'
218
+ '(-h --help)'{-h,--help}'[Show this help message and exit]'
219
+ )
220
+
221
+ _plexdo_subcommand() {
222
+ local m3u='--m3u[Also export an M3U file using Plex server paths]:m3u path:_files'
223
+ local pfx='(-p --prefix)'{-p,--prefix}'[Rewrite exported paths onto this prefix]:prefix:'
224
+ case ${words[1]} in
225
+ list-titles|list-library)
226
+ _arguments $_plexdo_global_opts \
227
+ '--album[Photo libraries only: restrict to one album]:album:{_plexdo_albums $(_plexdo_positional 0)}' \
228
+ '1:library id:_plexdo_libraries'
229
+ ;;
230
+ list-show)
231
+ _arguments $_plexdo_global_opts $pfx $m3u '1:show rating key:_plexdo_rating_keys'
232
+ ;;
233
+ show-metadata)
234
+ _arguments $_plexdo_global_opts '1:rating key:_plexdo_rating_keys'
235
+ ;;
236
+ export-titles)
237
+ _arguments $_plexdo_global_opts $pfx \
238
+ '--sort[Sort order]:order:(alpha date random)' \
239
+ '--album[Photo libraries only: restrict to one album]:album:{_plexdo_albums $(_plexdo_positional 0)}' \
240
+ '1:library id:_plexdo_libraries' \
241
+ '2:output path:_files'
242
+ ;;
243
+ search)
244
+ _arguments $_plexdo_global_opts \
245
+ '--media-type[Restrict to one media type]:type:(movie show episode track photo album artist)' \
246
+ '--library-id[Restrict to one library]:library id:_plexdo_libraries' \
247
+ '1:user:_plexdo_users' \
248
+ '2:query:'
249
+ ;;
250
+ status)
251
+ _arguments $_plexdo_global_opts \
252
+ '--section[Show only one section]:section:(server sessions users accounts connections scans activities tasks)'
253
+ ;;
254
+ list-users|list-libraries|write-config-example)
255
+ _arguments $_plexdo_global_opts
256
+ ;;
257
+ list-playlists)
258
+ _arguments $_plexdo_global_opts '1:user:_plexdo_users'
259
+ ;;
260
+ list-playlist)
261
+ _arguments $_plexdo_global_opts $pfx $m3u \
262
+ '1:user:_plexdo_users' \
263
+ '2:playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}'
264
+ ;;
265
+ export-playlist)
266
+ _arguments $_plexdo_global_opts $pfx \
267
+ '1:user:_plexdo_users' \
268
+ '2:playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}' \
269
+ '3:output path:_files'
270
+ ;;
271
+ remove-playlist)
272
+ _arguments $_plexdo_global_opts \
273
+ '1:user:_plexdo_users' \
274
+ '2:playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}'
275
+ ;;
276
+ append-playlist)
277
+ _arguments $_plexdo_global_opts \
278
+ '1:user:_plexdo_users' \
279
+ '2:playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}' \
280
+ '*:rating key:_plexdo_rating_keys'
281
+ ;;
282
+ read)
283
+ _arguments $_plexdo_global_opts \
284
+ '1:library id:_plexdo_libraries' \
285
+ '2:rating key:_plexdo_rating_keys'
286
+ ;;
287
+ find-missing)
288
+ _arguments $_plexdo_global_opts \
289
+ '(-l --library)'{-l,--library}'[Library to look in]:library:_plexdo_libraries' \
290
+ '(-A --all)'{-A,--all}'[Check every show]' \
291
+ '(-s --season)'{-s,--season}'[Season number, or several comma separated]:seasons:' \
292
+ '--include-specials[Also check season 0]' \
293
+ '1:show:_plexdo_rating_keys'
294
+ ;;
295
+ rescan)
296
+ _arguments $_plexdo_global_opts \
297
+ '(-s --status)'{-s,--status}'[Print all active scan jobs]' \
298
+ '(-n --now)'{-n,--now}'[Cancel pending scans before rescanning]' \
299
+ '1:library id:_plexdo_libraries'
300
+ ;;
301
+ build-interleaved|build-chronological)
302
+ _arguments $_plexdo_global_opts $pfx $m3u \
303
+ '(-o --overwrite)'{-o,--overwrite}'[Replace an existing playlist of the same name]' \
304
+ '1:playlist name:' \
305
+ '*:rating key:_plexdo_rating_keys'
306
+ ;;
307
+ build-randomize)
308
+ _arguments $_plexdo_global_opts $pfx $m3u \
309
+ '(-o --overwrite)'{-o,--overwrite}'[Replace an existing playlist of the same name]' \
310
+ '1:user:_plexdo_users' \
311
+ '2:source playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}' \
312
+ '3:destination playlist:'
313
+ ;;
314
+ copy-playlist-all-users)
315
+ _arguments $_plexdo_global_opts \
316
+ '(-o --overwrite)'{-o,--overwrite}'[Overwrite an existing playlist of the same name]' \
317
+ '1:source user:_plexdo_users' \
318
+ '2:source playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}'
319
+ ;;
320
+ copy-playlist-to-user)
321
+ _arguments $_plexdo_global_opts \
322
+ '(-o --overwrite)'{-o,--overwrite}'[Overwrite an existing playlist of the same name]' \
323
+ '1:source user:_plexdo_users' \
324
+ '2:source playlist:{_plexdo_playlists_or_keys $(_plexdo_positional 0)}' \
325
+ '3:target user:_plexdo_users' \
326
+ '4:destination playlist:'
327
+ ;;
328
+ copy-watched)
329
+ _arguments $_plexdo_global_opts \
330
+ '(-1 --one-way)'{-1,--one-way}'[Only write to the second user]' \
331
+ '(-l --library)'{-l,--library}'[Restrict to one library]:library id:_plexdo_libraries' \
332
+ '(-t --title)'{-t,--title}'[Restrict to one item]:rating key:_plexdo_rating_keys' \
333
+ '--unwatch[Propagate the unwatched state instead]' \
334
+ '1:first user:_plexdo_users' \
335
+ '2:second user:_plexdo_users'
336
+ ;;
337
+ login)
338
+ _arguments $_plexdo_global_opts \
339
+ '(-u --username)'{-u,--username}'[Plex username or email]:username:' \
340
+ '(-p --password)'{-p,--password}'[Plex password (INSECURE: visible in ps and shell history)]:password:' \
341
+ '(-c --code)'{-c,--code}'[Two-factor authentication code]:code:' \
342
+ '(-2 --two-factor)'{-2,--two-factor}'[Prompt for a two-factor code]'
343
+ ;;
344
+ *)
345
+ _arguments $_plexdo_global_opts
346
+ ;;
347
+ esac
348
+ }
349
+
350
+ _plexdo() {
351
+ local curcontext="$curcontext" state line
352
+ typeset -A opt_args
353
+ _arguments -C $_plexdo_global_opts \
354
+ '1:command:_plexdo_commands' \
355
+ '*::command argument:->subcmd'
356
+ [[ $state == subcmd ]] && _plexdo_subcommand
357
+ }
358
+
359
+ _plexdo "$@"