plexdo 2.0.3__tar.gz → 3.0.0__tar.gz
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.
- {plexdo-2.0.3 → plexdo-3.0.0}/AI.prompt.txt +568 -120
- {plexdo-2.0.3 → plexdo-3.0.0}/CHANGELOG.md +181 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/Makefile +3 -2
- {plexdo-2.0.3 → plexdo-3.0.0}/PKG-INFO +225 -91
- {plexdo-2.0.3 → plexdo-3.0.0}/README.md +224 -90
- {plexdo-2.0.3/src/plexdo/data → plexdo-3.0.0/completions}/_plexdo +192 -112
- {plexdo-2.0.3 → plexdo-3.0.0}/completions/plexdo.bash +210 -122
- {plexdo-2.0.3/src/plexdo/data → plexdo-3.0.0/completions}/plexdo.fish +180 -92
- {plexdo-2.0.3/src/plexdo/data → plexdo-3.0.0/completions}/plexdo.ps1 +148 -83
- plexdo-3.0.0/man/plexdo-album-list.1.scd +155 -0
- plexdo-3.0.0/man/plexdo-albums-list.1.scd +230 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-conventions.7.scd +149 -29
- plexdo-2.0.3/man/plexdo-list-libraries.1.scd → plexdo-3.0.0/man/plexdo-libraries-list.1.scd +14 -8
- plexdo-2.0.3/man/plexdo-list-titles.1.scd → plexdo-3.0.0/man/plexdo-library-list.1.scd +27 -20
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-login.1.scd +18 -3
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-logout.1.scd +6 -0
- plexdo-3.0.0/man/plexdo-merge-build.1.scd +211 -0
- plexdo-2.0.3/man/plexdo-find-missing.1.scd → plexdo-3.0.0/man/plexdo-missing-find.1.scd +17 -11
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-player-remote-play.1.scd +11 -5
- plexdo-3.0.0/man/plexdo-playlist-build.1.scd +305 -0
- plexdo-2.0.3/man/plexdo-clean-playlist.1.scd → plexdo-3.0.0/man/plexdo-playlist-clean.1.scd +20 -13
- plexdo-3.0.0/man/plexdo-playlist-copy.1.scd +212 -0
- plexdo-2.0.3/man/plexdo-export-playlist.1.scd → plexdo-3.0.0/man/plexdo-playlist-export.1.scd +17 -11
- plexdo-2.0.3/man/plexdo-list-playlist.1.scd → plexdo-3.0.0/man/plexdo-playlist-list.1.scd +17 -11
- plexdo-2.0.3/man/plexdo-remove-playlist.1.scd → plexdo-3.0.0/man/plexdo-playlist-remove.1.scd +19 -14
- plexdo-2.0.3/man/plexdo-list-playlists.1.scd → plexdo-3.0.0/man/plexdo-playlists-list.1.scd +14 -8
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-read.1.scd +18 -15
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-rescan.1.scd +46 -11
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-search.1.scd +18 -5
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-set-pin.1.scd +7 -2
- plexdo-2.0.3/man/plexdo-list-show.1.scd → plexdo-3.0.0/man/plexdo-show-list.1.scd +16 -10
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-show-metadata.1.scd +10 -4
- plexdo-2.0.3/man/plexdo-copy-watched.1.scd → plexdo-3.0.0/man/plexdo-show-watched-cp.1.scd +42 -22
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-status.1.scd +8 -2
- plexdo-2.0.3/man/plexdo-export-titles.1.scd → plexdo-3.0.0/man/plexdo-titles-export.1.scd +20 -12
- plexdo-2.0.3/man/plexdo-list-users.1.scd → plexdo-3.0.0/man/plexdo-users-list.1.scd +21 -10
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-wait.1.scd +6 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/man/plexdo-write-config-example.1.scd +8 -2
- plexdo-3.0.0/man/plexdo-write-secrets-example.1.scd +180 -0
- {plexdo-2.0.3/src/plexdo/data → plexdo-3.0.0/man}/plexdo.1 +351 -200
- {plexdo-2.0.3 → plexdo-3.0.0}/pyproject.toml +1 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/sbom.spdx.json +76 -76
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/accounts.py +25 -6
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/arguments.py +17 -15
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/cache.py +31 -0
- plexdo-3.0.0/src/plexdo/cli.py +362 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/__init__.py +51 -2
- plexdo-3.0.0/src/plexdo/commands/albums.py +424 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/auth.py +135 -25
- plexdo-3.0.0/src/plexdo/commands/build.py +628 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/copy.py +94 -53
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/libraries.py +19 -22
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/metadata.py +3 -6
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/missing.py +43 -8
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/playlists.py +59 -101
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/remote.py +1 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/rescan.py +56 -27
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/search.py +68 -13
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/status.py +3 -20
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/stream.py +9 -24
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/users.py +7 -14
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/watched.py +87 -26
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/config.py +1 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/connection.py +2 -2
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/constants.py +56 -2
- {plexdo-2.0.3/completions → plexdo-3.0.0/src/plexdo/data}/_plexdo +192 -112
- {plexdo-2.0.3/man → plexdo-3.0.0/src/plexdo/data}/plexdo.1 +351 -200
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/data/plexdo.bash +210 -122
- {plexdo-2.0.3/completions → plexdo-3.0.0/src/plexdo/data}/plexdo.fish +180 -92
- {plexdo-2.0.3/completions → plexdo-3.0.0/src/plexdo/data}/plexdo.ps1 +148 -83
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/fanout.py +8 -8
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/formats.py +1 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/identify.py +24 -0
- plexdo-3.0.0/src/plexdo/logs.py +92 -0
- plexdo-3.0.0/src/plexdo/photos.py +163 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/playback.py +1 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/playlists.py +6 -8
- plexdo-3.0.0/src/plexdo/profiling.py +409 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/secret_store.py +159 -14
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/sections.py +5 -5
- plexdo-3.0.0/src/plexdo/security.py +173 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/titles.py +16 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo.egg-info/PKG-INFO +225 -91
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo.egg-info/SOURCES.txt +23 -19
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/conftest.py +2 -0
- plexdo-3.0.0/tests/test_albums.py +560 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_cli_and_config.py +336 -35
- plexdo-3.0.0/tests/test_debug_log.py +280 -0
- plexdo-3.0.0/tests/test_fixes.py +747 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_identify.py +4 -4
- plexdo-3.0.0/tests/test_photos.py +122 -0
- plexdo-3.0.0/tests/test_playlists.py +268 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_records_missing.py +1 -1
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_secret_store.py +242 -1
- plexdo-3.0.0/tests/test_watched.py +179 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tools/wiki-pages.sh +1 -1
- plexdo-2.0.3/man/plexdo-append-playlist.1.scd +0 -118
- plexdo-2.0.3/man/plexdo-build-chronological.1.scd +0 -177
- plexdo-2.0.3/man/plexdo-build-concatenated.1.scd +0 -181
- plexdo-2.0.3/man/plexdo-build-interleaved.1.scd +0 -172
- plexdo-2.0.3/man/plexdo-build-randomize.1.scd +0 -164
- plexdo-2.0.3/man/plexdo-copy-playlist-all-users.1.scd +0 -151
- plexdo-2.0.3/man/plexdo-copy-playlist-to-user.1.scd +0 -145
- plexdo-2.0.3/src/plexdo/cli.py +0 -187
- plexdo-2.0.3/src/plexdo/commands/build.py +0 -311
- plexdo-2.0.3/src/plexdo/logs.py +0 -26
- plexdo-2.0.3/src/plexdo/photos.py +0 -75
- plexdo-2.0.3/src/plexdo/security.py +0 -67
- plexdo-2.0.3/tests/test_fixes.py +0 -236
- plexdo-2.0.3/tests/test_playlists.py +0 -170
- plexdo-2.0.3/tests/test_watched.py +0 -75
- {plexdo-2.0.3 → plexdo-3.0.0}/.github/workflows/ci.yml +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/.github/workflows/publish.yml +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/.github/workflows/wiki.yml +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/.markdownlint-cli2.yaml +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/.nvmrc +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/.yamllint.yaml +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/AI.policy.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/CONTRIBUTING.md +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/LICENSE +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/LICENSES/GPL-3.0-or-later.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/MANIFEST.in +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/REUSE.toml +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/SECURITY.md +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/package-lock.json +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/package.json +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/requirements.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/setup.cfg +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/__init__.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/__main__.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/airdates.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/commands/wait.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/console.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/convert.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/gallery.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/m3u.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/migrate.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/paths.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/py.typed +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/records.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/sorting.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/throttle.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo/tokens.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo.egg-info/dependency_links.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo.egg-info/entry_points.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo.egg-info/requires.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/src/plexdo.egg-info/top_level.txt +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_airdates_misc.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_console.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_fanout.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_formats.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_logout.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_logs.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_paths_tokens.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_playback.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_remote_play.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_throttle.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tests/test_wait.py +0 -0
- {plexdo-2.0.3 → plexdo-3.0.0}/tools/derive.py +0 -0
|
@@ -300,11 +300,21 @@ nothing to do with this version.
|
|
|
300
300
|
the range its dependent declares. A vulnerability in a development-only
|
|
301
301
|
npm dependency is fixed by upgrading the package that pulls it in. One
|
|
302
302
|
that no consistent tree avoids is left, with the reason recorded here:
|
|
303
|
-
KaTeX's GHSA-238p-pmpm-9mq7 (low), reached through
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
303
|
+
- KaTeX's GHSA-238p-pmpm-9mq7 (low), reached through
|
|
304
|
+
`markdownlint -> micromark-extension-math`, whose `^0.16` range no
|
|
305
|
+
patched KaTeX release satisfies. markdownlint imports only that
|
|
306
|
+
package's syntax extension; its KaTeX renderer is loaded and never
|
|
307
|
+
called, and plexdo ships none of it.
|
|
308
|
+
- re2js's GHSA-gg6h-894w-4m9c (high), quadratic time matching a
|
|
309
|
+
lookbehind, fixed in 2.8.7. markdownlint-cli2 0.24.0 pins markdownlint
|
|
310
|
+
0.42.0, which pins re2js 2.8.6 exactly; markdownlint 0.42.1 moves to
|
|
311
|
+
2.9.1, so the fix arrives with the markdownlint-cli2 release that takes
|
|
312
|
+
it up, and is not forced in early. markdownlint's own patterns (MD011,
|
|
313
|
+
MD013, MD039, and the default front-matter pattern) have no lookbehind,
|
|
314
|
+
`.markdownlint-cli2.yaml` adds no custom rule or pattern, and the only
|
|
315
|
+
input is this repository's own Markdown, so the slow path is not
|
|
316
|
+
reached. Check for that markdownlint-cli2 release before each commit,
|
|
317
|
+
and drop this entry once it is taken.
|
|
308
318
|
- A linter suppression covers one line and one rule, with a comment saying
|
|
309
319
|
why the code cannot change instead.
|
|
310
320
|
- One Google rule is knowingly not followed: its guide imports modules, not
|
|
@@ -378,14 +388,18 @@ src/plexdo/
|
|
|
378
388
|
__main__.py python -m plexdo
|
|
379
389
|
cli.py build_parser, main
|
|
380
390
|
constants.py windows_app_dir, default_paths, Paths, PATHS, CONFIG_PATH,
|
|
381
|
-
DEFAULT_CACHE_DIR, DEFAULT_SECRETS_FILE,
|
|
382
|
-
|
|
383
|
-
|
|
391
|
+
DEFAULT_CACHE_DIR, DEFAULT_SECRETS_FILE, session_file,
|
|
392
|
+
DEFAULT_SESSION_FILE, STORE_CHOICES, config_text,
|
|
393
|
+
CONFIG_EXAMPLE, SECRETS_EXAMPLE, LOG
|
|
394
|
+
logs.py configure_logging, open_debug_log, RedactingFormatter,
|
|
395
|
+
TimestampedFormatter
|
|
396
|
+
profiling.py profiled, span, usage, Run, Usage
|
|
384
397
|
config.py load_config, parse_config, cached_config, config_optional,
|
|
385
398
|
user_option, configured_users, expand_environment,
|
|
386
399
|
check_file_permissions, Config
|
|
387
|
-
secret_store.py backend,
|
|
388
|
-
|
|
400
|
+
secret_store.py backend, session_backend, sessions_are_apart,
|
|
401
|
+
open_store, load, save, describe, KeyringStore,
|
|
402
|
+
FileStore, SessionStore, Store
|
|
389
403
|
tokens.py load_store, lookup, store_token, admin_token,
|
|
390
404
|
forget_tokens, saved_password, save_password,
|
|
391
405
|
forget_logins, saved_pin, save_pin
|
|
@@ -394,7 +408,7 @@ src/plexdo/
|
|
|
394
408
|
arguments.py add_user_argument, add_library_argument,
|
|
395
409
|
add_m3u_argument, add_overwrite_argument,
|
|
396
410
|
add_rating_keys_argument, add_rating_key_option
|
|
397
|
-
cache.py cache_dir, write_cache
|
|
411
|
+
cache.py cache_dir, write_cache, cache_key, write_word_caches
|
|
398
412
|
convert.py parse_date, format_duration
|
|
399
413
|
console.py output, output_format, clean_text, table_limit,
|
|
400
414
|
print_table, print_metadata, stream_record,
|
|
@@ -402,22 +416,24 @@ src/plexdo/
|
|
|
402
416
|
formats.py to_json, to_yaml, to_csv, to_clixml, render
|
|
403
417
|
records.py loaded_fields, file_paths, release_date, summary_row,
|
|
404
418
|
cache_row, season_records
|
|
405
|
-
identify.py resolve_identifier
|
|
419
|
+
identify.py resolve_identifier, remember_words, argument_words
|
|
406
420
|
accounts.py server_for_user, account_type, resolve_user_arguments,
|
|
407
|
-
user_roster,
|
|
421
|
+
user_roster, user_rows, UserAccessError,
|
|
422
|
+
connect_with_token,
|
|
408
423
|
connect_as_shared_user, configured_entries,
|
|
409
424
|
configured_account, configured_accounts
|
|
410
425
|
sections.py resolve_section, resolve_sections,
|
|
411
426
|
resolve_library_arguments
|
|
412
427
|
titles.py display_title, find_item, fetch_item, fetch_show,
|
|
413
428
|
non_special_episodes, shuffle_list, item_is_played,
|
|
414
|
-
item_view_offset
|
|
429
|
+
item_view_offset, as_rating_key
|
|
415
430
|
playlists.py resolve_playlist, finalize_playlist, copy_playlist_to,
|
|
416
431
|
existing_playlist, preview_rows, require_items,
|
|
417
432
|
create_for_user
|
|
418
433
|
fanout.py add_user_target_arguments, deliver_playlist, run_per_user,
|
|
419
434
|
report_line
|
|
420
|
-
photos.py
|
|
435
|
+
photos.py walk_photo_albums, AlbumVisit, is_album, album_children,
|
|
436
|
+
collect_photos, collect_library_items, photo_file_path
|
|
421
437
|
sorting.py apply_sort
|
|
422
438
|
m3u.py write_m3u
|
|
423
439
|
gallery.py write_gallery_html
|
|
@@ -426,7 +442,8 @@ src/plexdo/
|
|
|
426
442
|
throttle.py paced, throttle_delay
|
|
427
443
|
playback.py fetch_address, parse_ip, url_host, url_of, play_fields,
|
|
428
444
|
add_play_arguments, Candidate, IPAddress
|
|
429
|
-
security.py scrub_password_argument
|
|
445
|
+
security.py scrub_password_argument, redact, redact_headers,
|
|
446
|
+
hide_in_logs, hide_request_secrets
|
|
430
447
|
data/ the four completions and the man page, as package data
|
|
431
448
|
commands/ one module per group (see COMMANDS)
|
|
432
449
|
```
|
|
@@ -445,14 +462,14 @@ Every module in `commands/` exposes exactly three names:
|
|
|
445
462
|
every `add_parser` call so each inherits the global flags.
|
|
446
463
|
- `COMMANDS` maps command name to handler.
|
|
447
464
|
- `REQUIRES_PLEX` is the subset needing a server; `auth` has an empty
|
|
448
|
-
frozenset because `login` and `write
|
|
449
|
-
exists, `logout` must work once the server no longer
|
|
450
|
-
`set-pin` only touches the secret store.
|
|
465
|
+
frozenset because `login` and the two `write-*-example` commands run
|
|
466
|
+
before a token exists, `logout` must work once the server no longer
|
|
467
|
+
accepts one, and `set-pin` only touches the secret store.
|
|
451
468
|
|
|
452
|
-
`commands/__init__.py` holds a `MODULES` tuple
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
`cli.py` never changes.
|
|
469
|
+
`commands/__init__.py` holds a `MODULES` tuple, `register_all(sub, parents)`,
|
|
470
|
+
`build_registry()`, and `GROUPS`, which is what `--help` lists the commands
|
|
471
|
+
from (see COMMAND NAMES AND HELP). Adding a command means adding a module,
|
|
472
|
+
listing it, and naming the command in one group; `cli.py` never changes.
|
|
456
473
|
|
|
457
474
|
## PLATFORM DEFAULTS
|
|
458
475
|
|
|
@@ -550,6 +567,37 @@ removes its entry.
|
|
|
550
567
|
- `secret_store.backend()` is chosen once per run and works before the
|
|
551
568
|
config exists, with the defaults, since the migration saves secrets
|
|
552
569
|
before writing the file.
|
|
570
|
+
- **With `secret_store: file` set outright, sessions are kept apart from
|
|
571
|
+
passwords.** `SESSION_KINDS` is `("tokens",)`; `session_backend()` is the
|
|
572
|
+
store for those kinds and `backend()` the store for the rest, and
|
|
573
|
+
`load`, `save`, and `describe(kind)` pick between them. Under `auto` and
|
|
574
|
+
`keyring` the two are the same store, a file `auto` fell back to
|
|
575
|
+
included. Under `file` the secrets file holds `passwords` and `pins`
|
|
576
|
+
alone, and the tokens go in a `SessionStore`, a `FileStore` at
|
|
577
|
+
`constants.DEFAULT_SESSION_FILE`: `$XDG_RUNTIME_DIR/plexdo/session.json`
|
|
578
|
+
when that variable is an absolute path, else `plexdo-UID/session.json`
|
|
579
|
+
under `tempfile.gettempdir()`, which honours `TMPDIR`, `TEMP`, and `TMP`
|
|
580
|
+
(`PlexDo/session.json` there on Windows). `session_file(is_windows, env,
|
|
581
|
+
tmpdir, uid)` takes all four as arguments so every case is testable.
|
|
582
|
+
The reasoning: that setting is for a machine with no keyring to unlock,
|
|
583
|
+
where the secrets file is all that guards a saved password, and a token,
|
|
584
|
+
which changes with every login and which plex.tv can reissue, does not
|
|
585
|
+
belong beside it or need to outlive a reboot.
|
|
586
|
+
- The temporary directory is writable by other accounts, so the session
|
|
587
|
+
directory is created with mode 0700 and then checked with `lstat`: it
|
|
588
|
+
must be a real directory, not a link, owned by the user. One of the
|
|
589
|
+
user's own that is open to group or other is tightened. Otherwise
|
|
590
|
+
writing is a `sys.exit` and nothing is written, and a session file
|
|
591
|
+
found there is ignored with a warning rather than believed. Windows has
|
|
592
|
+
neither check; its temporary directory is inside the user's profile.
|
|
593
|
+
- The first time the session store is opened, a `tokens` entry still in
|
|
594
|
+
the secrets file, where it was kept before, is moved to the session
|
|
595
|
+
file, a token already there winning, with a warning saying so. An
|
|
596
|
+
upgrade must not sign anyone out.
|
|
597
|
+
- `login` prints one note on **stderr**, in every format, when the two
|
|
598
|
+
stores differ: where sessions are kept, that they are apart from the
|
|
599
|
+
passwords and PINs, and that a session lasts until that file is cleared,
|
|
600
|
+
as at a reboot, when `plexdo login` is needed again.
|
|
553
601
|
|
|
554
602
|
`tokens.py` is the only module that knows the three kinds.
|
|
555
603
|
|
|
@@ -582,7 +630,7 @@ that finds its account early signs in no more. When all three fail, raise
|
|
|
582
630
|
`UserAccessError` explaining that admin rights do not override per-user
|
|
583
631
|
scoping and that a `users` entry signed in once with `login --username`
|
|
584
632
|
would fix it. It must be an ordinary `Exception`, **not** a `sys.exit`:
|
|
585
|
-
`
|
|
633
|
+
`playlist-copy --all` catches per-user failures and continues, and
|
|
586
634
|
`SystemExit` would tear down the whole run. `cli.main` converts it to a
|
|
587
635
|
clean exit for single-user commands. It carries a one-line `summary` so a
|
|
588
636
|
loop reports each skip in one line.
|
|
@@ -639,9 +687,9 @@ present.
|
|
|
639
687
|
|
|
640
688
|
## GLOBAL FLAGS
|
|
641
689
|
|
|
642
|
-
`-f/--format`, `--json`, `-v/--verbose`, `--debug`, `--
|
|
643
|
-
`--throttle`, `-V/--version`. They must work
|
|
644
|
-
command name**.
|
|
690
|
+
`-f/--format`, `--json`, `-v/--verbose`, `--debug`, `--debug-log PATH`,
|
|
691
|
+
`--dry-run`, `-W/--wide`, `--throttle`, `-V/--version`. They must work
|
|
692
|
+
**either before or after the command name**.
|
|
645
693
|
|
|
646
694
|
Implement with `_add_global_flags(parser, suppress=False)` called twice: once
|
|
647
695
|
on the top-level parser with ordinary defaults, and once on an
|
|
@@ -650,16 +698,92 @@ on the top-level parser with ordinary defaults, and once on an
|
|
|
650
698
|
The SUPPRESS default is essential, not cosmetic. A subparser parses into its
|
|
651
699
|
own namespace and then copies **every** attribute onto the main one, so
|
|
652
700
|
ordinary defaults on the inherited copies would silently clobber a flag given
|
|
653
|
-
before the subcommand: `plexdo --json list
|
|
701
|
+
before the subcommand: `plexdo --json users-list` would print a table.
|
|
702
|
+
|
|
703
|
+
Add them to an argument group titled `global options`, in order of their
|
|
704
|
+
long names, on both parsers. argparse carries a parent's groups over by
|
|
705
|
+
title, so every command's help lists them apart from its own options.
|
|
654
706
|
|
|
655
707
|
`--format` writes to `dest="format"`; `--json` is a `store_const` alias
|
|
656
708
|
writing to that same destination, so there is one attribute and no way for the
|
|
657
|
-
two to disagree. All logging goes to stderr only
|
|
709
|
+
two to disagree. All logging goes to stderr only, or to the `--debug-log`
|
|
710
|
+
file; never to stdout.
|
|
711
|
+
|
|
712
|
+
### The debug log
|
|
713
|
+
|
|
714
|
+
`--debug-log PATH` sends everything `--debug` would show to PATH instead,
|
|
715
|
+
and stderr keeps the level it would have without `--debug`; `--debug` is
|
|
716
|
+
not needed with it. `logs.open_debug_log` opens it with `os.open`,
|
|
717
|
+
`O_APPEND | O_CREAT`, mode 0600, so a new file is its owner's alone and
|
|
718
|
+
earlier runs are kept, and line buffered, so a killed run loses nothing it
|
|
719
|
+
logged; a path that cannot be opened is a `sys.exit` naming it, before the
|
|
720
|
+
command runs. Expand `~`.
|
|
721
|
+
|
|
722
|
+
`TimestampedFormatter` starts **every line**, not every record, with
|
|
723
|
+
`time.time_ns()` at the moment it is formatted, then `LEVEL logger:
|
|
724
|
+
message`; a traceback or a cProfile table carries the stamp on each line.
|
|
725
|
+
`LogRecord.created` is a float and cannot hold nanoseconds at today's epoch,
|
|
726
|
+
so it is not used.
|
|
727
|
+
|
|
728
|
+
`profiling.profiled(enabled, command)` is the context `cli.main` runs the
|
|
729
|
+
command in, after `scrub_password_argument` so no `--password` reaches the
|
|
730
|
+
run start line: a `profiling.Run` with `--debug-log`, `nullcontext()`
|
|
731
|
+
without. `profiling.span(name)` times one phase - `migrate`, `connect`,
|
|
732
|
+
`resolve-arguments`, `command:NAME` - and is a no-op with no Run active.
|
|
733
|
+
The Run writes, as `key=value` pairs so the file is as easy to grep as to
|
|
734
|
+
read:
|
|
735
|
+
- `plexdo.profile: run start` - plexdo, Python, plexapi, and requests
|
|
736
|
+
versions, platform, pid, command, `json.dumps(sys.argv)`;
|
|
737
|
+
- `span` lines - wall, CPU, user and system time, `cpu_percent`, memory,
|
|
738
|
+
and the HTTP calls and wait inside the span;
|
|
739
|
+
- `plexdo.http: send` / `recv` / `fail` - Run replaces
|
|
740
|
+
`requests.Session.send` for the run and restores it after, so every
|
|
741
|
+
session, plexapi's included, is seen. `send` has an id, method, URL,
|
|
742
|
+
body size, and headers; `recv` the status, reason, `wait_ns` (send to
|
|
743
|
+
whole response, by `perf_counter_ns`), `headers_ns` (requests'
|
|
744
|
+
`elapsed`), body size and type, and headers; `fail` the error, before
|
|
745
|
+
re-raising. Never read the body of a `stream=True` response, which would
|
|
746
|
+
consume what `read` streams: give its Content-Length. A redirect comes
|
|
747
|
+
back through `send`; a thread-local depth keeps the nested hops out of
|
|
748
|
+
the totals, so no wait counts twice;
|
|
749
|
+
- `resources` every `SAMPLE_SECONDS` (5) from a daemon thread, over the
|
|
750
|
+
interval since the last;
|
|
751
|
+
- `run end` - status (`SystemExit` code, `interrupted`,
|
|
752
|
+
`exception:TYPE`), totals, the HTTP count, wait, its share of the run,
|
|
753
|
+
and the slowest request, then cProfile's top `TOP_FUNCTIONS` (40) by
|
|
754
|
+
cumulative and by own time. Python 3.12 allows one profiler at a time,
|
|
755
|
+
so `enable()` raising `ValueError` is logged and the run goes on
|
|
756
|
+
without the tables.
|
|
757
|
+
CPU is `time.process_time_ns()` with `os.times()` for the user/system split.
|
|
758
|
+
Memory without a dependency: current RSS from `/proc/self/statm` on Linux
|
|
759
|
+
and `GetProcessMemoryInfo` through ctypes on Windows, the peak from
|
|
760
|
+
`resource.getrusage` (`ru_maxrss`, KiB except bytes on macOS) or Windows;
|
|
761
|
+
Linux updates the peak lazily, so report at least the current figure, and
|
|
762
|
+
`None` where the platform gives nothing. psutil would be a dependency for
|
|
763
|
+
one feature.
|
|
764
|
+
|
|
765
|
+
Redaction is in `security`. Both handlers, stderr and file, use
|
|
766
|
+
`RedactingFormatter`, which redacts the **whole formatted record**,
|
|
767
|
+
traceback included, since urllib3 logs request lines with their query
|
|
768
|
+
string and requests' exceptions quote URLs. `redact` replaces: the value of
|
|
769
|
+
any `name=value`, `name: value`, or quoted-key pair whose name is a token,
|
|
770
|
+
password, passwd, secret (as a suffix: `X-Plex-Token`, `authToken`), `pin`,
|
|
771
|
+
`X-Plex-Pin`, `X-Plex-Code`, `verificationCode`, `Authorization`, or a
|
|
772
|
+
cookie header, its scheme with it; the password in `scheme://user:pass@`;
|
|
773
|
+
and every value `hide_in_logs` was given. That registry is fed by
|
|
774
|
+
`secret_store.load` and `save`, the password prompt and
|
|
775
|
+
`scrub_password_argument`, and `hide_request_secrets`, which `send` calls
|
|
776
|
+
before the request goes out with the secret-named query parameters and
|
|
777
|
+
headers, a Basic credential's decoded password included. Values under 8
|
|
778
|
+
characters are not registered: a four-digit PIN would blank out every
|
|
779
|
+
matching number; named, it is still redacted. Bodies are never logged,
|
|
780
|
+
only their sizes. Name the registry what it is (`_KnownSecrets`), per the
|
|
781
|
+
naming rule above.
|
|
658
782
|
|
|
659
783
|
## OUTPUT
|
|
660
784
|
|
|
661
785
|
`output(data, args)` dispatches on `output_format(args)`, accepting **either**
|
|
662
|
-
a single record or a list so `show-metadata` and `list
|
|
786
|
+
a single record or a list so `show-metadata` and `library-list` share the path.
|
|
663
787
|
No command reaches for `json.dumps` directly.
|
|
664
788
|
|
|
665
789
|
### Formats
|
|
@@ -721,11 +845,12 @@ no benefit. Announce once at info level with the expected duration and how to
|
|
|
721
845
|
turn it off.
|
|
722
846
|
|
|
723
847
|
Apply wherever a loop makes one request per element and the count can exceed
|
|
724
|
-
50: the seasons walk in `list
|
|
725
|
-
state in `
|
|
726
|
-
source in `build
|
|
727
|
-
`
|
|
728
|
-
|
|
848
|
+
50: the seasons walk in `library-list`, `missing-find -A`, applying watched
|
|
849
|
+
state in `show-watched-cp`, removing entries in `playlist-clean`, reading each
|
|
850
|
+
source in `merge-build`, `collect_photos`, the walk of a show library in
|
|
851
|
+
`collect_library_items`, and the per-user loop in
|
|
852
|
+
`fanout.run_per_user`, which serves `playlist-copy --all`, and
|
|
853
|
+
`playlist-build` and `merge-build` given `-a`. Functions needing it take an optional `args`.
|
|
729
854
|
|
|
730
855
|
## RECORDS
|
|
731
856
|
|
|
@@ -768,12 +893,15 @@ content.
|
|
|
768
893
|
|
|
769
894
|
Every argument naming an **existing** playlist accepts a title or a ratingKey
|
|
770
895
|
through `resolve_playlist`; no command may call `plex.playlist(name)`
|
|
771
|
-
directly.
|
|
896
|
+
directly. What counts as a ratingKey is decided by `titles.as_rating_key`,
|
|
897
|
+
**ASCII digits and nothing else**: `int()` is too generous a test, taking
|
|
898
|
+
`1_000`, `+7`, ` 12 `, and digits of other scripts, any of which may be what
|
|
899
|
+
a playlist is called. A numeric value that is no item at all is a clean `sys.exit`, not
|
|
772
900
|
plexapi's `NotFound` escaping as a traceback: look it up with
|
|
773
901
|
`titles.find_item`, which returns None, and `fetch_item` wraps that for every
|
|
774
902
|
other fatal lookup, `fetch_show` included. Nothing calls `fetchItem` bare.
|
|
775
903
|
|
|
776
|
-
Every
|
|
904
|
+
Every command that creates a playlist constructs the list fully in memory, validates it is
|
|
777
905
|
non-empty, prints a numbered preview, and makes exactly one `createPlaylist`
|
|
778
906
|
call. `finalize_playlist` enforces this and returns `"created"` or
|
|
779
907
|
`"replaced"`.
|
|
@@ -799,32 +927,151 @@ Without `--overwrite`, a taken name falls back to appending `" admin copy"`;
|
|
|
799
927
|
if that is taken too the target is skipped with a warning and nothing is
|
|
800
928
|
touched.
|
|
801
929
|
|
|
930
|
+
## COMMAND NAMES AND HELP
|
|
931
|
+
|
|
932
|
+
A command is named `<object>-<action>`: what it acts on, then what it does
|
|
933
|
+
to it (`playlist-list`, `playlist-export`, `libraries-list`). Plural objects
|
|
934
|
+
list many (`playlists-list USER`), singular ones show one (`playlist-list
|
|
935
|
+
USER PLAYLIST`). The exceptions are fixed names, kept exactly:
|
|
936
|
+
`merge-build`, `show-watched-cp`, `missing-find`, `titles-export`, the
|
|
937
|
+
single words (`search`, `read`, `rescan`, `status`, `wait`, `login`,
|
|
938
|
+
`logout`), and `show-metadata`, `player-remote-play`, `set-pin`,
|
|
939
|
+
`write-config-example`, and `write-secrets-example`. There are no aliases:
|
|
940
|
+
one command, one name, so `COMMANDS` has exactly one entry per command.
|
|
941
|
+
|
|
942
|
+
Releases up to 2.1.0-rc.1 named commands `<action>-<object>`. 3.0.0 renamed
|
|
943
|
+
them, an incompatible change and so a MAJOR one, and also
|
|
944
|
+
folded `build-interleaved`, `build-chronological`, `build-randomize`, and
|
|
945
|
+
`append-playlist` into `playlist-build`, and `copy-playlist-all-users` and
|
|
946
|
+
`copy-playlist-to-user` into `playlist-copy`, and removed
|
|
947
|
+
`build-concatenated` and `list-titles` (whose alias `list-library` became
|
|
948
|
+
`library-list`). None of the old names is accepted.
|
|
949
|
+
|
|
950
|
+
`plexdo --help` lists the commands under five headings, in this order, and
|
|
951
|
+
sorted by name within each. `commands.GROUPS` is that list, and every
|
|
952
|
+
command must be in exactly one group (a test holds it to the registry):
|
|
953
|
+
|
|
954
|
+
| Group | Commands |
|
|
955
|
+
| --- | --- |
|
|
956
|
+
| libraries and titles | `album-list`, `albums-list`, `libraries-list`, `library-list`, `missing-find`, `search`, `show-list`, `show-metadata`, `titles-export` |
|
|
957
|
+
| playlists | `merge-build`, `playlist-build`, `playlist-clean`, `playlist-copy`, `playlist-export`, `playlist-list`, `playlist-remove`, `playlists-list` |
|
|
958
|
+
| playback and watched state | `player-remote-play`, `read`, `show-watched-cp`, `wait` |
|
|
959
|
+
| server | `rescan`, `status`, `users-list` |
|
|
960
|
+
| account and configuration | `login`, `logout`, `set-pin`, `write-config-example`, `write-secrets-example` |
|
|
961
|
+
|
|
962
|
+
argparse can only print subcommands as one flat list in registration order,
|
|
963
|
+
so pass `help=argparse.SUPPRESS` to `add_subparsers` and build the grouped
|
|
964
|
+
list into the parser's description, with `RawDescriptionHelpFormatter`:
|
|
965
|
+
the name padded to the longest, then the summary each `add_parser(help=...)`
|
|
966
|
+
gave, wrapped to 79 columns with a hanging indent. Give `add_subparsers`
|
|
967
|
+
`prog="plexdo"` and the parser `usage="%(prog)s [global options] <command>
|
|
968
|
+
[arguments]"`; without the first, every command's usage line repeats the
|
|
969
|
+
top-level usage. Summaries are one sentence, in the imperative, short
|
|
970
|
+
enough to fit their line. Positional metavars are upper case (`USER`,
|
|
971
|
+
`LIBRARY`, `PLAYLIST`, `NAME`, `QUERY`, `PATH`) except `ratingKey`, which is
|
|
972
|
+
spelled as Plex spells it.
|
|
973
|
+
|
|
974
|
+
After registration, `cli._tidy_command_help` goes over every command: it
|
|
975
|
+
sorts the `global options` group last, since a parent's groups otherwise
|
|
976
|
+
come before the groups a command adds, and, where a command set no `usage`
|
|
977
|
+
of its own, formats one from the command's own actions and appends
|
|
978
|
+
`[global options]`, so nine inherited flags do not bury the two that
|
|
979
|
+
matter. Both reach into argparse's protected attributes, which has no
|
|
980
|
+
public way to do either; keep that in the one function.
|
|
981
|
+
|
|
982
|
+
`playlist-build` and `playlist-copy` take a flag that decides what their
|
|
983
|
+
positional arguments mean, which argparse cannot express. They collect the
|
|
984
|
+
positionals unnamed, set `check_arguments` with `set_defaults` to a
|
|
985
|
+
callable taking the namespace, and `cli.parse_arguments(parser, argv)`,
|
|
986
|
+
which `main` uses in place of `parse_args`, runs it: the callable names the
|
|
987
|
+
arguments for the mode (`args.user_id`, `args.name`, `args.rating_keys`,
|
|
988
|
+
...) or calls `parser.error`, exit 2. That happens before anything connects
|
|
989
|
+
and before user titles are resolved, so `resolve_user_arguments` finds
|
|
990
|
+
`user_id` where it expects it. Each has a hand-written multi-line `usage`
|
|
991
|
+
and a description saying what each mode takes.
|
|
992
|
+
|
|
802
993
|
## COMMANDS
|
|
803
994
|
|
|
804
995
|
### Listing and searching
|
|
805
996
|
|
|
806
|
-
- `list
|
|
807
|
-
- `list
|
|
808
|
-
`aliases=["list-library"]`. argparse reports whichever spelling was typed,
|
|
809
|
-
so `COMMANDS` needs an entry for both, giving 30 registry entries for 29
|
|
810
|
-
commands; the smoke test floor must allow for it. Table columns are
|
|
997
|
+
- `libraries-list` - columns `id`, `type`, `title`; writes the libraries cache.
|
|
998
|
+
- `library-list LIBRARY [--album A]` - table columns are
|
|
811
999
|
`ratingKey`, `title`, `releaseDate`, `rating`, `studio`; a machine-readable
|
|
812
1000
|
format carries every listed field and, for a show library, nests each show's
|
|
813
1001
|
episodes under a `seasons` object keyed by season name. Always cache the
|
|
814
1002
|
minimal `ratingKey`/`title` rows whatever the caller asked to see.
|
|
815
|
-
- `list
|
|
816
|
-
|
|
1003
|
+
- `albums-list LIBRARY` (`commands/albums.py`) - the albums of a music
|
|
1004
|
+
(section type `artist`) or photo library; any other type exits 1. Music
|
|
1005
|
+
albums come from `section.albums()`. A photo library is walked depth
|
|
1006
|
+
first from `section.all()`, opening each album with one
|
|
1007
|
+
`fetchItems(f"{album.key}/children")` and splitting the children by
|
|
1008
|
+
`TAG == "Directory"` into nested albums, walked next so they follow their
|
|
1009
|
+
parent, and contents; a ratingKey met twice is listed once. Table
|
|
1010
|
+
columns are `ratingKey`, `artist` (music only), `title`, `year`, `items`,
|
|
1011
|
+
`played`; a music table reads `leafCount`/`viewedLeafCount` from the
|
|
1012
|
+
listing and opens no album. Every other format, and every photo listing,
|
|
1013
|
+
opens each album through `paced`. A machine-readable record carries
|
|
1014
|
+
`ratingKey`, `artist`, `title`, `year`, `items`, `duration`, `state`,
|
|
1015
|
+
`played`, `playedItems`, `inProgressItems`, `lastViewedAt`, `progress`,
|
|
1016
|
+
and `contents`; each content record `index`, `ratingKey`, `type`, `disc`,
|
|
1017
|
+
`track`, `title`, `artist`, `duration`, `state`, `played`, `viewCount`,
|
|
1018
|
+
`lastViewedAt`, `viewOffset`, `progress`, `files`. Every key is always
|
|
1019
|
+
present, null where it does not apply. Fields are read from `vars()`, so
|
|
1020
|
+
nothing reloads. An item has a state only if it carries `viewCount`
|
|
1021
|
+
(tracks and clips; photos do not): `in progress` when `viewOffset > 0`,
|
|
1022
|
+
even if played before, else `played` when `viewCount > 0`, else
|
|
1023
|
+
`unplayed`. An album is `in progress` if any item is, `played` if every
|
|
1024
|
+
playable item is, `unplayed` if none is, else `in progress`; null with no
|
|
1025
|
+
playable item. `progress` is a percentage to one decimal, null unless the
|
|
1026
|
+
state is `in progress`: an item's resume point over its duration; an
|
|
1027
|
+
album's played running time (played items in full, items in progress up
|
|
1028
|
+
to their resume point) over the total of its playable items. `viewOffset`
|
|
1029
|
+
is likewise null unless in progress.
|
|
1030
|
+
- `album-list RATINGKEY` (also `commands/albums.py`) - one album, by
|
|
1031
|
+
`fetch_item` and one `/children` fetch. Refused with exit 1 unless the
|
|
1032
|
+
item is a Directory whose `type` is `album` (music) or `photo` (a photo
|
|
1033
|
+
album; a photo shares the type but not the TAG), since shows, seasons,
|
|
1034
|
+
and artists are Directories too. Nested photo albums are split off,
|
|
1035
|
+
logged at INFO, and counted in the table summary as `nested albums`, not
|
|
1036
|
+
listed. JSON and YAML output the same `album_record` as `albums-list`;
|
|
1037
|
+
CSV and CLIXML output its `contents` alone. A table prints the album as a
|
|
1038
|
+
metadata block with only the keys that apply (`ratingKey`, `artist`,
|
|
1039
|
+
`title`, `year`, `items`, `nested albums`, `length`, `state`, `played` as
|
|
1040
|
+
"N of M", `in progress`, `progress` as "P%", `last played`), a blank
|
|
1041
|
+
line, then one row per item: `#`, `ratingKey`, `track` (as `2-03` when
|
|
1042
|
+
the album spans several discs; music only), `title`, `artist` (only when
|
|
1043
|
+
some track's artist differs from the album's), `length`, then, unless the
|
|
1044
|
+
album holds photos alone, `state`, `progress` ("3:20 / 9:46 (34.1%)" for
|
|
1045
|
+
an item in progress, else empty), and `last played` as a date.
|
|
1046
|
+
- `show-list RATINGKEY [--m3u P] [-p PREFIX]` - fails fast if not a `Show`.
|
|
1047
|
+
- `users-list` - `id`, `type`, `title`, `username`, `email`, from
|
|
1048
|
+
`accounts.user_rows`, which the `users` section of `status` shows too, so
|
|
1049
|
+
the two cannot come to differ; the command once printed three of the
|
|
1050
|
+
five while its documentation promised all of them. `account_type`
|
|
817
1051
|
returns managed/home/friend/shared. plexapi exposes `restricted` as the raw
|
|
818
1052
|
XML **string**, so `"0"` is truthy and a naive check labels every account
|
|
819
1053
|
managed.
|
|
820
|
-
- `list
|
|
1054
|
+
- `playlists-list USER`, `playlist-list USER PLAYLIST [--m3u P] [-p PREFIX]`.
|
|
821
1055
|
- `show-metadata RATINGKEY` - type-specific builders via `_METADATA_BUILDERS`
|
|
822
1056
|
keyed on `item.type`, plus the file paths. A table names a lone file `file`
|
|
823
1057
|
and numbers several `file 1`, `file 2`; other formats carry a `files` list.
|
|
824
|
-
- `search USER QUERY [--media-type T] [--library-id L]` -
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1058
|
+
- `search USER QUERY [--media-type T] [--library-id L]` - with
|
|
1059
|
+
`--media-type`, search only the libraries whose section type can hold it
|
|
1060
|
+
(`episode` in `show`, `track` and `album` in `artist`): asked for a type
|
|
1061
|
+
it cannot hold, plexapi raises "Unknown libtype", once per library. A
|
|
1062
|
+
library named with `--library-id` that cannot hold the type is a
|
|
1063
|
+
`sys.exit`. A library whose search raises is skipped, logged at DEBUG,
|
|
1064
|
+
and the rest are searched; the failures are reported, as **one** warning
|
|
1065
|
+
starting `Search failed in`, **only when no result came back at all**.
|
|
1066
|
+
With results in hand a library that could not be searched is noise.
|
|
1067
|
+
- `missing-find SHOW [-l LIBRARY] [-A] [-s SEASONS] [--include-specials]` -
|
|
1068
|
+
names one show by default, resolved through `resolve_identifier`. A SHOW
|
|
1069
|
+
written as a ratingKey is fetched outright first, and used if it is a
|
|
1070
|
+
show: listing every show of every library to look up a number was the
|
|
1071
|
+
slow part. One `section.search(title=..., libtype="show")` per show
|
|
1072
|
+
library is still made, to give the usual warning when another show is
|
|
1073
|
+
*titled* that number ("24", "1923"). A number that is no show's key falls
|
|
1074
|
+
through to the roster, where it may be a title. `-A`
|
|
828
1075
|
sweeps every show and refuses a SHOW argument; `-s` takes one number or a
|
|
829
1076
|
comma list and applies only to a single show. For each season the run starts
|
|
830
1077
|
at 0 when an episode 0 exists and otherwise at 1, ending at the highest
|
|
@@ -837,41 +1084,81 @@ touched.
|
|
|
837
1084
|
|
|
838
1085
|
### Building and copying
|
|
839
1086
|
|
|
840
|
-
- `build-
|
|
841
|
-
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
which
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
- `
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
- `
|
|
1087
|
+
- `playlist-build (-I | -C) NAME RATINGKEY...`,
|
|
1088
|
+
`playlist-build -R USER SOURCE DEST`,
|
|
1089
|
+
`playlist-build -A USER PLAYLIST RATINGKEY...` - one command, four modes,
|
|
1090
|
+
as a required mutually exclusive group of `store_const` flags writing
|
|
1091
|
+
`dest="mode"`. The positionals are `first` and `rest` (`nargs="+"`), both
|
|
1092
|
+
shown as `ARG`, so that an option between the first argument and the
|
|
1093
|
+
others still parses; `_check_build` names them for the mode. The wrong
|
|
1094
|
+
count for the mode, or a ratingKey that is not an integer, is a usage
|
|
1095
|
+
error, exit 2, as is `-A` with any of `-o`, `-u`, `-a`, `--m3u`, and
|
|
1096
|
+
`-p`, which only make sense for a playlist being created. With too few
|
|
1097
|
+
arguments for `first` and `rest` at all, argparse reports only "the
|
|
1098
|
+
following arguments are required: ARG", before any flag is looked at;
|
|
1099
|
+
the parser's `error` is wrapped to say instead what each mode takes,
|
|
1100
|
+
naming together the modes that take the same ("-I and -C take ..."). `-A` is
|
|
1101
|
+
`--append`; `-a` stays `--all-users`.
|
|
1102
|
+
- `-I/--interleaved` - round-robin across shows.
|
|
1103
|
+
- `-C/--chronological` - sorted by air date. A missing date
|
|
1104
|
+
is estimated from neighbours in the same season: collect up to 6 either
|
|
1105
|
+
side, require at least 3 known dates (so at least 2 intervals), take the
|
|
1106
|
+
median interval, estimate from latest-previous `+median` and
|
|
1107
|
+
earliest-next `-median`, averaging when both exist, and prompt only when
|
|
1108
|
+
that is impossible. A film with no date has no neighbours, so it is
|
|
1109
|
+
prompted for directly. `prompt_for_date` takes **any** item: name an
|
|
1110
|
+
episode by show, season, and episode, with `??` for a number Plex lacks
|
|
1111
|
+
rather than `00`, which would read as a real episode 0, and anything
|
|
1112
|
+
else by title and year. It once read episode-only attributes and crashed
|
|
1113
|
+
on every undated film. Running out of input is a `sys.exit` naming the
|
|
1114
|
+
item, not an `EOFError`.
|
|
1115
|
+
- `-R/--randomize` - Fisher-Yates via `secrets.randbelow`.
|
|
1116
|
+
- `-A/--append` - fetches every ratingKey as the user sees the server,
|
|
1117
|
+
exits 1 if any is not found, previews, then adds them in one call.
|
|
1118
|
+
Refuse a smart playlist before fetching anything, as `playlist-clean`
|
|
1119
|
+
does: plexapi raises `BadRequest` from `addItems`, which is also
|
|
1120
|
+
caught there and turned into a `sys.exit`.
|
|
1121
|
+
- `merge-build USER NAME FIRST SECOND [--ratio A:B] [-s] [-U]` - mixes two
|
|
1122
|
+
playlists in turn, A items from FIRST then B from SECOND, default 1:1,
|
|
1123
|
+
each keeping its own order. Parse the ratio in argparse (`type=`), so a
|
|
1124
|
+
bad one is a usage error, exit 2: two ASCII whole numbers above 0 joined
|
|
1125
|
+
by `:`. A side has run out on the first turn it has nothing to give; a
|
|
1126
|
+
turn cut short because fewer items were left still counts. From that turn
|
|
1127
|
+
the rest of the other follows by default, rather than being dropped, and
|
|
1128
|
+
`--verbose` reports how many items follow unmixed; `-s/--stop-short`
|
|
1129
|
+
leaves them out instead, the turn included, and reports how many. Sizes
|
|
1130
|
+
that match the ratio run out together, with no tail, so `-s` changes
|
|
1131
|
+
nothing. Repeats are kept; `-U/--unique` passes over an item already
|
|
1132
|
+
taken, by ratingKey, and the next item of the same side takes its place,
|
|
1133
|
+
so the ratio holds, and under `-s` a side left with only repeats has run
|
|
1134
|
+
out. `-U`, not `-u`: `-u` is `--user`, as on `playlist-build`. Read both
|
|
1135
|
+
sources before writing anything, so either may be the destination under
|
|
1136
|
+
`--overwrite`: the items are already in memory when the old playlist
|
|
1137
|
+
goes. Warn about an empty side rather than passing over it: a typo that
|
|
1138
|
+
resolved to the wrong playlist looks exactly like a playlist that happens
|
|
1139
|
+
to be empty.
|
|
1140
|
+
- `playlist-copy -A USER PLAYLIST [-o]`,
|
|
1141
|
+
`playlist-copy -T USER PLAYLIST TO_USER DEST [-o]` - one command, two
|
|
1142
|
+
modes, as a required mutually exclusive group writing `dest="copy_to"`.
|
|
1143
|
+
The positionals are `source_user_id`, `source_playlist`, and `target`
|
|
1144
|
+
(`nargs="*"`); `_check_copy` requires none for `-A` and exactly two for
|
|
1145
|
+
`-T`, naming them `user_id` and `dest`, and anything else is a usage
|
|
1146
|
+
error, exit 2.
|
|
1147
|
+
- `-A/--all` - skips the source user, prints the
|
|
1148
|
+
item list **once** before the loop and passes `preview=False`
|
|
1149
|
+
thereafter, then one line per user (`created`, `replaced`, `skipped`,
|
|
1150
|
+
`failed`) printed as it completes. Because the caller surfaces the skip
|
|
1151
|
+
reason, the message inside `copy_playlist_to` is `LOG.info`, not a
|
|
1152
|
+
warning that would duplicate every skip on stderr.
|
|
1153
|
+
- `-T/--to` - a table shows the item list then the outcome line; a
|
|
1154
|
+
machine-readable format shows the outcome record **alone**, as `-A`
|
|
1155
|
+
does, because two documents on one stream parse as neither.
|
|
1156
|
+
- `playlist-remove USER PLAYLIST` - names the playlist by its title in what
|
|
1157
|
+
it prints, however it was given. `--dry-run` says on stderr which
|
|
1158
|
+
playlist would be deleted, by title and ratingKey: a rehearsal that
|
|
1159
|
+
prints nothing cannot be told from one that found nothing to do, and
|
|
1160
|
+
every `--dry-run` must say what it would have done without `--verbose`.
|
|
1161
|
+
- `playlist-clean USER PLAYLIST [--include-partial]` - removes the watched
|
|
875
1162
|
entries and leaves the rest in their existing order. Preview first, then one
|
|
876
1163
|
`removeItems` call per entry so the loop is paced like any other per-item
|
|
877
1164
|
loop; the playlist item IDs come from the listing plexapi already holds, so
|
|
@@ -880,26 +1167,29 @@ touched.
|
|
|
880
1167
|
the removal list, so it lines up with what Plex shows. A part-played item is
|
|
881
1168
|
**kept** by default - it is the one being watched right now, not a watched
|
|
882
1169
|
one - and `--include-partial` drops it, labelled `partial` rather than
|
|
883
|
-
`played`.
|
|
1170
|
+
`played`. **The resume point is looked at first**: an item played before
|
|
1171
|
+
and part way through again has a view count and a resume point, and it is
|
|
1172
|
+
the second that says the user is in the middle of it. Refuse a smart playlist: its contents come from a filter, and Plex
|
|
884
1173
|
answers a delete on one of its entries with a 400. Warn when every entry is
|
|
885
1174
|
going, since Plex drops a playlist that loses its last item.
|
|
886
1175
|
|
|
887
1176
|
#### Who a built playlist is for
|
|
888
1177
|
|
|
889
|
-
|
|
890
|
-
through `fanout.add_user_target_arguments` as a
|
|
891
|
-
writing the pair out on each parser is what trips
|
|
892
|
-
either, the playlist goes
|
|
893
|
-
from, which is the admin server for
|
|
894
|
-
source user's server for
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1178
|
+
`playlist-build`, except under `-A`, and `merge-build` take `-u/--user` and
|
|
1179
|
+
`-a/--all-users`, registered through `fanout.add_user_target_arguments` as a
|
|
1180
|
+
mutually exclusive group - writing the pair out on each parser is what trips
|
|
1181
|
+
`duplicate-code`. Without either, the playlist goes to the account the items
|
|
1182
|
+
came from, which is the admin server for `-I` and `-C`, which take rating
|
|
1183
|
+
keys, and the source user's server for `-R` and `merge-build`, which take
|
|
1184
|
+
playlists. `deliver_playlist`
|
|
1185
|
+
takes both servers for exactly that reason. `-a` targets
|
|
1186
|
+
`accounts.user_roster`, which **includes the admin** at id 0, because the
|
|
1187
|
+
admin is a user of the server too.
|
|
898
1188
|
|
|
899
1189
|
`fanout.run_per_user` holds the loop: print the item list once, then run a
|
|
900
1190
|
per-user action and print its one-line outcome as it completes, so a long run
|
|
901
1191
|
shows progress; in a machine-readable format emit one record per user instead,
|
|
902
|
-
since one document cannot carry both. `
|
|
1192
|
+
since one document cannot carry both. `playlist-copy --all` uses the same
|
|
903
1193
|
loop with its own action and its own target list - the accounts a playlist is
|
|
904
1194
|
shared with rather than the roster - so the loop is not written twice.
|
|
905
1195
|
|
|
@@ -911,7 +1201,7 @@ others left.
|
|
|
911
1201
|
|
|
912
1202
|
### Watched state
|
|
913
1203
|
|
|
914
|
-
`
|
|
1204
|
+
`show-watched-cp USER USER [-1] [-l LIBRARY] [-t KEY] [--unwatch]`.
|
|
915
1205
|
`_select_winner(first, second, unwatch)` returns `(winner, loser)` or None:
|
|
916
1206
|
|
|
917
1207
|
| situation | default | `--unwatch` |
|
|
@@ -924,13 +1214,38 @@ A `lastViewedAt` of `None` must never win: map it to `datetime.min` when the
|
|
|
924
1214
|
latest wins and `datetime.max` when the earliest does. Plex rewrites
|
|
925
1215
|
`viewOffset` continuously during playback, so treat offsets within
|
|
926
1216
|
`_OFFSET_TOLERANCE_MS` (10s) as identical, or every run reports spurious
|
|
927
|
-
changes.
|
|
1217
|
+
changes.
|
|
1218
|
+
|
|
1219
|
+
A state is two things, **the played flag and the resume point, and both are
|
|
1220
|
+
copied as they stand**; the view count is used only to tell played from
|
|
1221
|
+
not, and is never copied. So two states match when the flags are equal and
|
|
1222
|
+
the offsets are within tolerance, whether or not the item is played: an item
|
|
1223
|
+
played before and part way through again differs from one simply played.
|
|
1224
|
+
Two fully played states with no resume point need no sync at all.
|
|
1225
|
+
|
|
1226
|
+
`_planned_steps(winner, loser)` gives the calls that bring the loser to the
|
|
1227
|
+
winner's state, and the plan's `action` is those steps joined by `+`:
|
|
1228
|
+
`markPlayed` or `markUnplayed` when the flags differ; else `clearProgress`
|
|
1229
|
+
when the winner has no resume point and the loser has one; then
|
|
1230
|
+
`setProgress` when the winner has one. Marking played or unplayed clears
|
|
1231
|
+
the resume point on the server, so it comes **before** `updateProgress`,
|
|
1232
|
+
and `clearProgress` is done by marking the item what it already is. It was
|
|
1233
|
+
once applied only when the loser was fully played, so `--unwatch` planned
|
|
1234
|
+
`markUnplayed` for an item one user was part way through, called nothing,
|
|
1235
|
+
and reported the same change on every run.
|
|
1236
|
+
|
|
1237
|
+
`-t KEY` names what to compare. A movie, episode, track, or clip is
|
|
1238
|
+
compared by itself. A show or season stands for its `episodes()`, an artist
|
|
1239
|
+
or album for its `tracks()`, each member compared on its own: a
|
|
1240
|
+
container's own played flag only says whether **every** member is played,
|
|
1241
|
+
so comparing containers found two users at different points in a show
|
|
1242
|
+
identical. Any other type is a `sys.exit` saying it has no watched state.
|
|
928
1243
|
|
|
929
1244
|
plexapi renamed these methods, so call through a helper trying both spellings
|
|
930
1245
|
(`markPlayed`/`markWatched`, `markUnplayed`/`markUnwatched`) and read played
|
|
931
1246
|
state from `isPlayed`, then `isWatched`, then `viewCount`. That read and the
|
|
932
1247
|
resume-point read are `titles.item_is_played` and `titles.item_view_offset`,
|
|
933
|
-
because `clean
|
|
1248
|
+
because `playlist-clean` needs the same two and the rules must not be written
|
|
934
1249
|
twice. All three attributes default to a bool or a number rather than None, so
|
|
935
1250
|
reading them does not satisfy plexapi's reload condition.
|
|
936
1251
|
|
|
@@ -940,13 +1255,18 @@ Watch state lives on leaf items: map `movie`->`movie`, `show`->`episode`,
|
|
|
940
1255
|
|
|
941
1256
|
### Exporting
|
|
942
1257
|
|
|
943
|
-
- `export-
|
|
1258
|
+
- `playlist-export` and `playlist-list --m3u` are two ways to the same
|
|
1259
|
+
file, kept on purpose; both write through the one
|
|
1260
|
+
`commands.playlists._export_m3u`, so neither can write it differently.
|
|
1261
|
+
- `playlist-export USER PLAYLIST PATH`, `titles-export LIBRARY PATH
|
|
944
1262
|
[--sort alpha|date|random] [--album A]`.
|
|
945
1263
|
- M3U carries Plex server filesystem paths, `#EXTINF` seconds from
|
|
946
1264
|
`duration`/1000 with `-1` fallback, and skips items with no path.
|
|
947
1265
|
- Photo libraries export a self-contained Spotlight.js gallery. **No Plex HTTP
|
|
948
1266
|
URLs anywhere in it**: both `href` and `src` are server filesystem paths.
|
|
949
|
-
- `-p/--prefix` on
|
|
1267
|
+
- `-p/--prefix` on every command that exports (`playlist-list`, `show-list`,
|
|
1268
|
+
`playlist-export`, `titles-export`, `playlist-build`, `merge-build`),
|
|
1269
|
+
registered through
|
|
950
1270
|
`paths.add_prefix_argument` - repeating the help text inline trips
|
|
951
1271
|
`duplicate-code`. `mapper_for(plex, args)` returns `identity` without a
|
|
952
1272
|
prefix, so the default costs no extra call. The prefix replaces the
|
|
@@ -957,24 +1277,57 @@ Watch state lives on leaf items: map `movie`->`movie`, `show`->`episode`,
|
|
|
957
1277
|
|
|
958
1278
|
### Photos
|
|
959
1279
|
|
|
960
|
-
`
|
|
1280
|
+
`photos.walk_photo_albums(section, args)` is the one walk of a photo
|
|
1281
|
+
library, used by `albums-list`, `library-list`, and `titles-export`. It
|
|
1282
|
+
starts from `section.all()` and opens each album with one
|
|
1283
|
+
`fetchItems(f"{album.key}/children")`, splitting the children by
|
|
1284
|
+
`TAG == "Directory"` into nested albums, walked next so they follow their
|
|
1285
|
+
parent, and contents. It returns an `AlbumVisit` per album: the album, its
|
|
1286
|
+
contents, and the titles of the albums around it ending with its own. The
|
|
1287
|
+
photos of an album inside another are in neither the library listing nor
|
|
1288
|
+
the outer album's own children, so a walk that stops at the top level
|
|
1289
|
+
loses them, as `collect_photos` once did.
|
|
1290
|
+
|
|
1291
|
+
Two guards, both needed. A ratingKey met twice is passed over: that lists
|
|
1292
|
+
once an album the server shows at the top level as well as nested, and
|
|
1293
|
+
stops albums that contain one another from being walked for ever. And
|
|
1294
|
+
nesting is followed `MAX_ALBUM_DEPTH` (16) albums deep, since a loop check
|
|
1295
|
+
cannot stop a server that keeps producing new albums; what lies deeper is
|
|
1296
|
+
left out with **one** warning naming where the walk stopped.
|
|
1297
|
+
|
|
1298
|
+
`collect_photos` takes the `type == "photo"` contents of each visit,
|
|
1299
|
+
leaving clips out. `--album` matches a title, ignoring case, against every
|
|
1300
|
+
title in the visit's path, so naming an album takes the albums inside it
|
|
1301
|
+
too, and an album nested anywhere can be named. A name matching no album
|
|
1302
|
+
is a `sys.exit`; an album that exists and is empty is not.
|
|
961
1303
|
**Never** `section.search(libtype="photo")`: Plex requires a non-empty query
|
|
962
1304
|
string and returns nothing for an empty one, so the library appears empty.
|
|
963
1305
|
|
|
964
1306
|
### Streaming and server management
|
|
965
1307
|
|
|
966
|
-
- `read
|
|
967
|
-
`requests.get(stream=True)`.
|
|
1308
|
+
- `read RATINGKEY` - streams to `sys.stdout.buffer` in 64KB chunks via
|
|
1309
|
+
`requests.get(stream=True)`. It takes no library: a ratingKey is unique
|
|
1310
|
+
on the server, and an argument that only existed to be checked against
|
|
1311
|
+
the item was one more thing to look up and type.
|
|
968
1312
|
Catch `BrokenPipeError` silently and `os.dup2` the null device onto stdout,
|
|
969
1313
|
so the interpreter's final flush does not print "BrokenPipeError ignored"
|
|
970
1314
|
when the user quits the player. The epilog shows worked examples including
|
|
971
|
-
`plexdo read
|
|
1315
|
+
`plexdo read 12345 | mpv --vo=drm -` for **Linux with a display attached
|
|
972
1316
|
but no desktop environment** - describe the condition, not the machine:
|
|
973
1317
|
"headless" contradicts having a screen, and the hardware could be a Pi, a
|
|
974
1318
|
kiosk, a sign, or an old laptop.
|
|
975
|
-
- `rescan
|
|
976
|
-
method. `section.update()` scans for files;
|
|
977
|
-
metadata.
|
|
1319
|
+
- `rescan LIBRARY [-n]`, `rescan -a [-n]`, `rescan -s` - `plex.activities`
|
|
1320
|
+
is a **property**, not a method. `section.update()` scans for files;
|
|
1321
|
+
`refresh()` only re-fetches metadata. `-a/--all` scans every library with
|
|
1322
|
+
the one `plex.library.update()` call. Exactly one of LIBRARY and `-a` is
|
|
1323
|
+
required without `-s`, checked by a `check_arguments` hook so it is a
|
|
1324
|
+
usage error, exit 2, before anything connects: with neither, the message
|
|
1325
|
+
says to name a library **or use `--all` to rescan every library**, so a
|
|
1326
|
+
library left off by mistake does not set the whole server scanning.
|
|
1327
|
+
`--dry-run` says on stderr what would be scanned, and whether pending
|
|
1328
|
+
scans would be cancelled first. `-s` with no activity prints
|
|
1329
|
+
`No active scan jobs.` on stderr as a table and an **empty list** in a
|
|
1330
|
+
machine-readable format.
|
|
978
1331
|
- `status [--section S]` - the sessions table carries `sessionKey`, `user`,
|
|
979
1332
|
`library`, `ratingKey`, `title`, `state`, `player`, `platform`, `address`,
|
|
980
1333
|
`progress`, and `file`; `sessionKey` is what `wait --session` takes. `records.playing_file` reports the file in use: a session marks
|
|
@@ -1174,8 +1527,9 @@ command would not repeat them and trip `duplicate-code`.
|
|
|
1174
1527
|
none), and `--save-password` saves the password beside it so plexdo can
|
|
1175
1528
|
sign in again by itself once the token stops working. `--dry-run`
|
|
1176
1529
|
authenticates and saves nothing. The table says where the token went
|
|
1177
|
-
(`secret_store.describe()`); other formats give `user`, `store`,
|
|
1178
|
-
`
|
|
1530
|
+
(`secret_store.describe("tokens")`); other formats give `user`, `store`,
|
|
1531
|
+
`passwordStore` (`describe("passwords")`), `verified`, and
|
|
1532
|
+
`passwordSaved`. `--password` on the command line is
|
|
1179
1533
|
scrubbed from the process title via `ctypes.pythonapi.Py_GetArgcArgv`
|
|
1180
1534
|
(`sys.argv` is only a copy; `ps` reads the original buffer) and redacted
|
|
1181
1535
|
from `sys.argv`, with a warning that is blunt about what cannot be fixed:
|
|
@@ -1205,6 +1559,30 @@ command would not repeat them and trip `duplicate-code`.
|
|
|
1205
1559
|
prints the exact template, which needs `RawDescriptionHelpFormatter` for the
|
|
1206
1560
|
epilog's newlines; that same formatter stops argparse wrapping the
|
|
1207
1561
|
description, so pre-wrap it with `textwrap.fill(..., width=78)`.
|
|
1562
|
+
- `write-secrets-example [-o]` - writes `constants.SECRETS_EXAMPLE` to the
|
|
1563
|
+
path `plex: secrets_file` names, after `expanduser`, creating its
|
|
1564
|
+
directory. **When `secrets_file` is not set, write nothing**: say so on
|
|
1565
|
+
stderr, naming the config path, and print the example to stdout, exit 0.
|
|
1566
|
+
A missing config file sets nothing, so it takes that path too rather than
|
|
1567
|
+
the "Config not found" exit; read the file with `parse_config` directly,
|
|
1568
|
+
not `load_config`. The default secrets path is deliberately not used:
|
|
1569
|
+
only a path the user chose is written to. An existing file is refused
|
|
1570
|
+
without `-o/--overwrite`, before `--dry-run` as for the config, because
|
|
1571
|
+
it may hold the only copy of a saved password or PIN. Create the file
|
|
1572
|
+
with `os.open(..., O_CREAT | O_EXCL, 0o600)`, unlinking an old one first
|
|
1573
|
+
under `-o`, so it is never readable by anyone else, even for a moment,
|
|
1574
|
+
and an overwritten file does not keep looser permissions. The example is
|
|
1575
|
+
the **password store**, the secrets file as `secret_store: file` keeps
|
|
1576
|
+
it: JSON as `FileStore` writes it (`indent=2`, sorted keys), with
|
|
1577
|
+
`passwords` and `pins`, each with one placeholder entry, and a
|
|
1578
|
+
`_comment` entry, a list of short lines, saying every value is a
|
|
1579
|
+
placeholder and that session tokens are not kept there. It must hold no
|
|
1580
|
+
`tokens`: a placeholder token beside a real one made the store hold
|
|
1581
|
+
two, so `admin_token` no longer took "the only one" and every command
|
|
1582
|
+
reported no admin token. JSON has no comments; `FileStore` reads only the kinds it
|
|
1583
|
+
is asked for and writes back every entry it loaded, so the note is
|
|
1584
|
+
harmless and survives a later save. Its `--help` prints the exact
|
|
1585
|
+
example in the epilog. Needs no server.
|
|
1208
1586
|
|
|
1209
1587
|
## ERROR HANDLING
|
|
1210
1588
|
|
|
@@ -1219,7 +1597,21 @@ or where no player answers. A
|
|
|
1219
1597
|
command line argparse cannot parse exits 2, its own convention. An
|
|
1220
1598
|
interrupted `wait` exits 130. No bare
|
|
1221
1599
|
`except`; the broad ones are in `_cancel_all_scans`, the per-user copy loop,
|
|
1222
|
-
and per-section collection, each with a
|
|
1600
|
+
the per-library search, and per-section collection, each with a
|
|
1601
|
+
`# pylint: disable=broad-except`.
|
|
1602
|
+
|
|
1603
|
+
**A request that fails must not reach the user as a traceback.** `cli.main`
|
|
1604
|
+
wraps `_run` and catches `requests.exceptions.RequestException` and
|
|
1605
|
+
`plexapi.exceptions.PlexApiException`, the two families everything raised
|
|
1606
|
+
by a request to the server or to plex.tv belongs to, and exits 1 with one
|
|
1607
|
+
or two lines chosen by kind: the server could not be reached (check it is
|
|
1608
|
+
running, and `plex: url`), it did not answer in time, it refused the saved
|
|
1609
|
+
token (run `plexdo login`), or it refused the request. Only the **first
|
|
1610
|
+
line** of the error is shown, since plexapi attaches the server's whole
|
|
1611
|
+
HTML error page to some, and it goes through `security.redact`, since a
|
|
1612
|
+
URL in a requests error can carry a token. The traceback is logged at
|
|
1613
|
+
DEBUG. Handlers closer to the request, such as the per-user loops and
|
|
1614
|
+
`UserAccessError`, still take precedence; this is the net under them.
|
|
1223
1615
|
|
|
1224
1616
|
## PLATFORM SUPPORT
|
|
1225
1617
|
|
|
@@ -1247,6 +1639,40 @@ data. All cover the same values, read a 15-minute cache under the platform
|
|
|
1247
1639
|
cache directory (honouring `cache_dir`), and refresh a stale cache in the
|
|
1248
1640
|
background so completion never blocks.
|
|
1249
1641
|
|
|
1642
|
+
**Each script finds the cache as plexdo does**, or completion silently
|
|
1643
|
+
offers nothing: `plex: cache_dir` from `config.yaml`, in
|
|
1644
|
+
`$XDG_CONFIG_HOME/plexdo` or `~/.config/plexdo` (`%LOCALAPPDATA%\PlexDo` on
|
|
1645
|
+
Windows), with environment variables and a leading `~` expanded; else
|
|
1646
|
+
`$XDG_CACHE_HOME/plexdo`, by default `~/.cache/plexdo`. An XDG variable
|
|
1647
|
+
counts only when absolute. They once read the 1.x INI file and a
|
|
1648
|
+
hard-coded `~/.cache`, long after both had moved. PyYAML may not be
|
|
1649
|
+
importable by whichever python the script finds, and PowerShell has no
|
|
1650
|
+
YAML reader, so each reads the one scalar it needs a line at a time: a
|
|
1651
|
+
`cache_dir:` line indented under the top-level `plex:` key, skipping
|
|
1652
|
+
blank and comment lines, unquoting a double-quoted value as a JSON string
|
|
1653
|
+
and a single-quoted one by its doubled quotes, and cutting a trailing
|
|
1654
|
+
` #` comment from a plain one. bash, zsh, and fish share one python
|
|
1655
|
+
snippet, which must contain **no single quote and no backslash**, because
|
|
1656
|
+
fish holds it in a single-quoted string.
|
|
1657
|
+
|
|
1658
|
+
**The cache is kept twice, by ID and by word.** `titles.LIBRARY.json` and
|
|
1659
|
+
`playlists.USER.json` are named for the numeric ID, but a script holds
|
|
1660
|
+
whatever is on the command line, which may be a title, and cannot resolve
|
|
1661
|
+
it as plexdo does. So `cache.write_word_caches` files the same rows as
|
|
1662
|
+
`titles-by-word.WORD.json` and `playlists-by-word.WORD.json`, once for
|
|
1663
|
+
each spelling: the ID, the title, and the word as typed.
|
|
1664
|
+
`identify.remember_words` records the three while the argument is
|
|
1665
|
+
resolved, on `args.argument_words`, since resolution replaces what was
|
|
1666
|
+
typed with the ID; `argument_words(args, name)` reads them back. Both
|
|
1667
|
+
caches are written whenever a result is handled, so they cannot drift. In
|
|
1668
|
+
a file name `cache.cache_key` replaces each of `/ \ : * ? " < > |` and
|
|
1669
|
+
the control characters with `_`, chosen so every shell can make the same
|
|
1670
|
+
name with one substitution of its own; each script has that function and
|
|
1671
|
+
a test compares them. Playlist and album-name completion read the by-word
|
|
1672
|
+
cache for the word typed, dequoted; ratingKey completion still gathers
|
|
1673
|
+
every `titles.*.json`, which the by-word names do not match, so no key is
|
|
1674
|
+
offered twice.
|
|
1675
|
+
|
|
1250
1676
|
Numeric IDs are shown with their titles. Distinguish what is *insertable* from
|
|
1251
1677
|
what is merely *shown*: user and library titles are insertable because those
|
|
1252
1678
|
arguments accept them, but an item title is not, since `fetchItem` needs a
|
|
@@ -1268,7 +1694,19 @@ numeric ratingKey.
|
|
|
1268
1694
|
skipping options that consume a value. Guard the background refresh on the
|
|
1269
1695
|
binary existing: fish reports an unknown command *before* redirection
|
|
1270
1696
|
applies, scribbling on the prompt mid-completion.
|
|
1271
|
-
-
|
|
1697
|
+
- For `playlist-build` and `playlist-copy`, every script first finds the mode
|
|
1698
|
+
flag among the words already typed, and completes each positional as that
|
|
1699
|
+
mode defines it: with `-I` or `-C` a name then rating keys; with `-R` a
|
|
1700
|
+
user, then that user's playlists; with `-A` on `playlist-build` a user,
|
|
1701
|
+
that user's playlists, then rating keys; with `-T` on `playlist-copy` the
|
|
1702
|
+
receiving user third. With no mode typed yet, nothing says what an
|
|
1703
|
+
argument is, so bash offers the mode flags and the others offer only
|
|
1704
|
+
options. In zsh, hold option specs that use `{-o,--overwrite}` braces in
|
|
1705
|
+
**arrays**: a scalar assignment does not expand the braces, and the spec
|
|
1706
|
+
reaches `_arguments` malformed and is silently never offered.
|
|
1707
|
+
- **PowerShell** - `Register-ArgumentCompleter -Native`. Compare the mode
|
|
1708
|
+
flags with `-cin`: PowerShell's `-in` ignores case, and `-a` and `-A` are
|
|
1709
|
+
different options. Needs no external
|
|
1272
1710
|
interpreter: `ConvertFrom-Json` reads the cache. Return `CompletionResult`
|
|
1273
1711
|
objects so the ID is inserted and the title shows as a tooltip. Two traps
|
|
1274
1712
|
that parse cleanly and fail at runtime: a range of `0..-1` **wraps** and
|
|
@@ -1278,7 +1716,8 @@ numeric ratingKey.
|
|
|
1278
1716
|
## MAN PAGE
|
|
1279
1717
|
|
|
1280
1718
|
`man/plexdo.1`, in roff, mirrored into `src/plexdo/data/`. Documents every
|
|
1281
|
-
command, the
|
|
1719
|
+
command, under the same five headings and in the same order as `--help`,
|
|
1720
|
+
and the configuration and token formats, exit status, environment, files,
|
|
1282
1721
|
and worked examples, and passes `groff -man -Tutf8 -ww -z` with no warnings.
|
|
1283
1722
|
|
|
1284
1723
|
Escapes that silently corrupt copy-pasteable examples: a literal backslash is
|
|
@@ -1305,9 +1744,16 @@ double-width characters, the ASCII fallback, and fitting to a budget;
|
|
|
1305
1744
|
identifier resolution including the ID-versus-title collision; the overwrite
|
|
1306
1745
|
guard performing **no** server calls when it refuses; watched-state selection
|
|
1307
1746
|
in both directions and that an undated state never wins; which playlist entries
|
|
1308
|
-
`clean
|
|
1309
|
-
playlist rather than within the removal list; that
|
|
1310
|
-
order
|
|
1747
|
+
`playlist-clean` drops, and that it numbers them by their place in the
|
|
1748
|
+
playlist rather than within the removal list; that a merge keeps each side's
|
|
1749
|
+
order and that `--unique` matches on ratingKey; that every command is in
|
|
1750
|
+
exactly one help group, that `--help` lists them grouped and sorted, and that
|
|
1751
|
+
the old names are refused; that each `playlist-build` and `playlist-copy`
|
|
1752
|
+
mode names its arguments, and that the wrong count, a missing or second mode
|
|
1753
|
+
flag, and `--append` with a creating option all exit 2; that
|
|
1754
|
+
`write-secrets-example` prints rather than writes when `secrets_file` is not
|
|
1755
|
+
set, refuses an existing file, writes mode 0600, and that its example loads
|
|
1756
|
+
through `FileStore` with its note surviving a save; that a per-user create
|
|
1311
1757
|
reports a taken name instead of exiting and performs no call when it does, and
|
|
1312
1758
|
that a fan-out records an unreachable user and an unexpected failure and keeps
|
|
1313
1759
|
going; where `wait` puts its end point for every combination of marker,
|
|
@@ -1359,9 +1805,10 @@ because unknown and zero are different things.
|
|
|
1359
1805
|
packaged mirrors, and that no source file contains non-ASCII. Editing the
|
|
1360
1806
|
source copy and forgetting the mirror ships a stale asset in the wheel.
|
|
1361
1807
|
- `smoke` imports the package, builds the full parser, and asserts every
|
|
1362
|
-
module in `MODULES` still exposes `register()` and `COMMANDS
|
|
1808
|
+
module in `MODULES` still exposes `register()` and `COMMANDS`, and that
|
|
1809
|
+
the commands registered are exactly those in `GROUPS`. pylint scores
|
|
1363
1810
|
a module 10.00/10 even when an edit has truncated that tail, which breaks
|
|
1364
|
-
every command in it.
|
|
1811
|
+
every command in it; a count to compare against would only go stale.
|
|
1365
1812
|
|
|
1366
1813
|
The Makefile also provides `install`, `uninstall`, `develop`, `reinstall`,
|
|
1367
1814
|
`install-man`, `uninstall-man`, `install-completion` and its four per-shell
|
|
@@ -1408,8 +1855,9 @@ version cannot be undone. Strip a leading `v` before comparing.
|
|
|
1408
1855
|
## COMMAND PAGES
|
|
1409
1856
|
|
|
1410
1857
|
Beside the roff overview, every command has a detailed page in scdoc source
|
|
1411
|
-
format: `man/plexdo-COMMAND.1.scd`, one per command,
|
|
1412
|
-
|
|
1858
|
+
format: `man/plexdo-COMMAND.1.scd`, one per command, so `playlist-build`
|
|
1859
|
+
and `playlist-copy` each have one page covering every mode, with a `MODES`
|
|
1860
|
+
section and each argument marked with the modes that take it, and
|
|
1413
1861
|
`man/plexdo-conventions.7.scd` for what every command shares - the global
|
|
1414
1862
|
options, how users, libraries, playlists, items, and sessions are named,
|
|
1415
1863
|
output formats, path rewriting, pacing, exit status, files, and
|
|
@@ -1448,7 +1896,7 @@ and SEE ALSO. GLOBAL OPTIONS says what **each** global flag does for that
|
|
|
1448
1896
|
particular command - "no effect" is an answer, and `--dry-run` writing a
|
|
1449
1897
|
file anyway is worth saying. OPTION INTERACTIONS covers every conflict and
|
|
1450
1898
|
dependency, whether argparse enforces it (`-u`/`-a`) or the handler does
|
|
1451
|
-
(`find
|
|
1899
|
+
(`missing-find -A` with SHOW, `wait --now` without `-r`). Document what the
|
|
1452
1900
|
code does, checked against it, not what a help string implies. Sample tables
|
|
1453
1901
|
are real output from `print_table` with an ASCII console, never hand-drawn.
|
|
1454
1902
|
|