plextool 3.0.1__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.
- plextool-3.0.1/LICENSE +21 -0
- plextool-3.0.1/PKG-INFO +232 -0
- plextool-3.0.1/README.md +198 -0
- plextool-3.0.1/plextool/__init__.py +3 -0
- plextool-3.0.1/plextool/__main__.py +5 -0
- plextool-3.0.1/plextool/audit.py +142 -0
- plextool-3.0.1/plextool/auth.py +518 -0
- plextool-3.0.1/plextool/cli.py +52 -0
- plextool-3.0.1/plextool/common.py +274 -0
- plextool-3.0.1/plextool/dupes.py +190 -0
- plextool-3.0.1/plextool/ondemand.py +382 -0
- plextool-3.0.1/plextool/playlists.py +153 -0
- plextool-3.0.1/plextool/recent.py +203 -0
- plextool-3.0.1/plextool/resume.py +61 -0
- plextool-3.0.1/plextool/scan.py +80 -0
- plextool-3.0.1/plextool/server.py +438 -0
- plextool-3.0.1/plextool/stale.py +126 -0
- plextool-3.0.1/plextool/status.py +68 -0
- plextool-3.0.1/plextool/subs.py +338 -0
- plextool-3.0.1/plextool/watched.py +249 -0
- plextool-3.0.1/plextool.egg-info/PKG-INFO +232 -0
- plextool-3.0.1/plextool.egg-info/SOURCES.txt +32 -0
- plextool-3.0.1/plextool.egg-info/dependency_links.txt +1 -0
- plextool-3.0.1/plextool.egg-info/entry_points.txt +2 -0
- plextool-3.0.1/plextool.egg-info/requires.txt +10 -0
- plextool-3.0.1/plextool.egg-info/top_level.txt +1 -0
- plextool-3.0.1/pyproject.toml +49 -0
- plextool-3.0.1/setup.cfg +4 -0
- plextool-3.0.1/tests/test_auth.py +256 -0
- plextool-3.0.1/tests/test_ondemand.py +251 -0
- plextool-3.0.1/tests/test_plextool.py +196 -0
- plextool-3.0.1/tests/test_server.py +322 -0
- plextool-3.0.1/tests/test_subs.py +208 -0
- plextool-3.0.1/tests/test_tools.py +227 -0
plextool-3.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Milkmansson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
plextool-3.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plextool
|
|
3
|
+
Version: 3.0.1
|
|
4
|
+
Summary: Maintenance helpers for a Plex Media Server
|
|
5
|
+
Author: Milkmansson
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/milkmansson/plextool
|
|
8
|
+
Project-URL: Documentation, https://plextool.readthedocs.io/
|
|
9
|
+
Project-URL: Source, https://github.com/milkmansson/plextool
|
|
10
|
+
Project-URL: Issues, https://github.com/milkmansson/plextool/issues
|
|
11
|
+
Keywords: plex,plex-media-server,plexapi,recently-added,duplicates,watched,subtitles,cli
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Classifier: Topic :: System :: Systems Administration
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: plexapi>=4.18
|
|
26
|
+
Requires-Dist: pyjwt[crypto]
|
|
27
|
+
Provides-Extra: qr
|
|
28
|
+
Requires-Dist: segno; extra == "qr"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff; extra == "dev"
|
|
32
|
+
Requires-Dist: segno; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# plextool
|
|
36
|
+
|
|
37
|
+
> [!WARNING]
|
|
38
|
+
> Still in development/testing... please raise an issue if you find one!
|
|
39
|
+
|
|
40
|
+
Command line tools for Plex library maintenance, built on
|
|
41
|
+
[python-plexapi](https://python-plexapi.readthedocs.io/). They talk to the
|
|
42
|
+
server through its API. Nothing here touches the Plex database file, which is
|
|
43
|
+
too risky to edit by hand.
|
|
44
|
+
|
|
45
|
+
## What it is useful for
|
|
46
|
+
|
|
47
|
+
plextool is a command line toolkit for looking after a Plex Media Server: fixing
|
|
48
|
+
things Plex's web interface can't, and finding problems across a large library.
|
|
49
|
+
Typical jobs:
|
|
50
|
+
|
|
51
|
+
- **Fix the Recently Added order.** This is what it was first built for. Plex has no way to
|
|
52
|
+
edit the Recently Added lists, so after you rebuild the database, delete
|
|
53
|
+
duplicate files and rescan, or reorganise files on disk, everything shows up as
|
|
54
|
+
"new" and the real newest content is buried. [`plextool recent`](https://plextool.readthedocs.io/en/latest/commands/recent.html)
|
|
55
|
+
changes the "added at" date of movies, shows, seasons and episodes: set each
|
|
56
|
+
item back to its own release or air date, shift dates by an offset, or set one date for a
|
|
57
|
+
whole batch. The date is locked so a rescan can't undo it.
|
|
58
|
+
```console
|
|
59
|
+
$ plextool recent --recent 20 --release # preview: put the 20 newest back to their release dates
|
|
60
|
+
$ plextool recent --title "Some Show" --shift=-30d --commit
|
|
61
|
+
```
|
|
62
|
+
- **Find and remove duplicate movies and episodes.** [`plextool dupes`](https://plextool.readthedocs.io/en/latest/commands/dupes.html)
|
|
63
|
+
lists every item Plex holds more than one file for, and can delete the extras you don't want.
|
|
64
|
+
- **Free up disk space.** [`plextool stale`](https://plextool.readthedocs.io/en/latest/commands/stale.html) finds large, never-watched or
|
|
65
|
+
long-idle movies and shows.
|
|
66
|
+
- **Audit a library.** [`plextool audit`](https://plextool.readthedocs.io/en/latest/commands/audit.html) reports unmatched items, missing files,
|
|
67
|
+
missing posters or summaries, low-resolution video, and missing subtitle or audio languages.
|
|
68
|
+
- **Copy, back up and restore watched status.** [`plextool watched`](https://plextool.readthedocs.io/en/latest/commands/watched.html) moves watched
|
|
69
|
+
state between users (for example when a shared account is split), saves it to a file, and
|
|
70
|
+
restores it after a rebuild or when someone closes and reopens their Plex account.
|
|
71
|
+
- **Clear a stuck Continue Watching list.** [`plextool resume`](https://plextool.readthedocs.io/en/latest/commands/resume.html) lists and removes
|
|
72
|
+
entries that won't go away.
|
|
73
|
+
- **Back up and restore playlists.** [`plextool playlists`](https://plextool.readthedocs.io/en/latest/commands/playlists.html) saves video playlists
|
|
74
|
+
to a file and recreates them on the same or another server.
|
|
75
|
+
- **Get subtitles.** [`plextool subs`](https://plextool.readthedocs.io/en/latest/commands/subs.html) extracts the subtitle tracks inside MKV and MP4
|
|
76
|
+
files, searches Plex's subtitle providers for a language and downloads one, and, with
|
|
77
|
+
`--export`, copies the subtitle Plex downloaded out of its database into a file beside
|
|
78
|
+
the video so it works outside Plex too. Plex itself never writes that file.
|
|
79
|
+
- **Script Plex.** Reports can print JSON, and exports can go through a pipe, so the output
|
|
80
|
+
feeds other tools.
|
|
81
|
+
|
|
82
|
+
Everything that changes the server is a preview until you pass `--commit`. plextool uses Plex's API and
|
|
83
|
+
never edits the Plex database file. It signs in through your browser with
|
|
84
|
+
[`plextool auth`](https://plextool.readthedocs.io/en/latest/commands/auth.html), and works against a server on the same machine, across the
|
|
85
|
+
internet, or directly with no internet access ([`plextool server`](https://plextool.readthedocs.io/en/latest/commands/server.html)).
|
|
86
|
+
|
|
87
|
+
## Commands
|
|
88
|
+
|
|
89
|
+
| Command | What it does |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| [`plextool auth`](https://plextool.readthedocs.io/en/latest/commands/auth.html) | Sign in to Plex with a link or QR code, list and remove old sign-ins from your account, and wipe the saved login and keys. |
|
|
92
|
+
| [`plextool server`](https://plextool.readthedocs.io/en/latest/commands/server.html) | Find servers on your account or network and choose the one to use. Covers working offline or directly against the server. |
|
|
93
|
+
| [`plextool status`](https://plextool.readthedocs.io/en/latest/commands/status.html) | Server version, library sizes, who is watching what. |
|
|
94
|
+
| [`plextool scan`](https://plextool.readthedocs.io/en/latest/commands/scan.html) | Scan libraries or a single folder, refresh metadata, empty trash, optimize. |
|
|
95
|
+
| [`plextool recent`](https://plextool.readthedocs.io/en/latest/commands/recent.html) | Change "added at" dates to reorder Recently Added. Works on movies, shows, seasons and episodes. |
|
|
96
|
+
| [`plextool dupes`](https://plextool.readthedocs.io/en/latest/commands/dupes.html) | List movies and episodes that have more than one file, and optionally delete the extras. |
|
|
97
|
+
| [`plextool stale`](https://plextool.readthedocs.io/en/latest/commands/stale.html) | Find large, unwatched or long-idle content to free up space. |
|
|
98
|
+
| [`plextool audit`](https://plextool.readthedocs.io/en/latest/commands/audit.html) | Find unmatched items, missing files, missing posters or summaries, low resolution, missing subtitle or audio languages. |
|
|
99
|
+
| [`plextool watched`](https://plextool.readthedocs.io/en/latest/commands/watched.html) | Copy watched state between users, back it up and restore it, or mark a show or movie watched. |
|
|
100
|
+
| [`plextool resume`](https://plextool.readthedocs.io/en/latest/commands/resume.html) | List or clear stuck Continue Watching entries. |
|
|
101
|
+
| [`plextool playlists`](https://plextool.readthedocs.io/en/latest/commands/playlists.html) | Back up and restore video playlists. |
|
|
102
|
+
| [`plextool subs`](https://plextool.readthedocs.io/en/latest/commands/subs.html) | Extract subtitles from MKV and MP4 files, search Plex's subtitle providers for new ones and pick which to download, and copy subtitles Plex downloaded (kept in its database, not beside the video) out as files next to the video. Files are named for Plex, forced and SDH included. |
|
|
103
|
+
|
|
104
|
+
The `plextool` commands that change anything do a dry run unless you pass
|
|
105
|
+
`--commit`. Deleting files (`dupes --keep`) also asks you to type `DELETE`.
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
Install it from PyPI. This needs `python` installed as a prerequisite:
|
|
110
|
+
|
|
111
|
+
```console
|
|
112
|
+
$ python3 -m venv .venv && source .venv/bin/activate # optional: keeps plextool's packages separate
|
|
113
|
+
$ pip install 'plextool[qr]' # '[qr]' adds a QR code to the sign-in; leave it off if you don't want one
|
|
114
|
+
$ plextool auth login # open the link it prints (or scan the QR code) and approve
|
|
115
|
+
$ plextool server find # servers on your Plex account and on your network
|
|
116
|
+
$ plextool server use "My Server" # one of the names from the list
|
|
117
|
+
$ plextool status # libraries, and who is watching what right now
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`status` ends with what is playing:
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
Now playing (1 session, 0 transcoding)
|
|
124
|
+
alice: Some Movie (2020) [playing] on Living Room TV
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
To get, say, Swedish subtitles for that movie from the subtitle providers Plex searches:
|
|
128
|
+
|
|
129
|
+
```console
|
|
130
|
+
$ plextool subs fetch -t "Some Movie" --lang sv --list # see what is available; nothing is downloaded
|
|
131
|
+
$ plextool subs fetch -t "Some Movie" --lang sv --choose --commit # pick one; Plex downloads it
|
|
132
|
+
$ plextool subs fetch -t "Some Movie" --lang sv --choose --export --commit # same, and also save a file beside the video
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Where the subtitle ends up.** When Plex downloads a subtitle from a provider, it keeps
|
|
136
|
+
the copy in its own database. It does not put a file next to the video, so only Plex can
|
|
137
|
+
show it: another player, or another Plex server, never sees it. The first two commands stop
|
|
138
|
+
there, and you pick the subtitle from the player's subtitle menu.
|
|
139
|
+
|
|
140
|
+
**`--export` gets around that.** Once Plex has downloaded the subtitle, plextool copies
|
|
141
|
+
Plex's stored copy out of the server and saves it as an ordinary file beside the video
|
|
142
|
+
(`<video name>.sv.srt`), which any player can use. It doesn't read the video file, so the
|
|
143
|
+
MKV or MP4 is never opened, and it doesn't contact the subtitle provider. (Subtitle
|
|
144
|
+
tracks that are already inside an MKV or MP4 are a different job, for `plextool subs extract`.)
|
|
145
|
+
For `--export`, the machine you run it on must be able to read the media files. If it sees
|
|
146
|
+
them under a different path than Plex does, use `--map`. The
|
|
147
|
+
[`subs` page](https://plextool.readthedocs.io/en/latest/commands/subs.html#where-each-subtitle-comes-from) sets out where each kind of subtitle comes from.
|
|
148
|
+
|
|
149
|
+
For a TV show, `-t "Some Show"` covers every episode, which is a search for each one,
|
|
150
|
+
so start with `--list`.
|
|
151
|
+
|
|
152
|
+
If the server is on the machine you are typing on, skip `server find` and `server use`:
|
|
153
|
+
plextool tries `http://localhost:32400` by default. Without internet access, see
|
|
154
|
+
[working offline](https://plextool.readthedocs.io/en/latest/commands/server.html#working-offline-or-directly-against-the-server).
|
|
155
|
+
|
|
156
|
+
## Getting help
|
|
157
|
+
|
|
158
|
+
Every command explains itself, with options and examples:
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
$ plextool --help
|
|
162
|
+
$ plextool recent --help
|
|
163
|
+
$ plextool watched copy --help
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
A fuller page for each command is on [Read the Docs](https://plextool.readthedocs.io/)
|
|
167
|
+
(linked in the table above); its source is in `docs/source/commands/`.
|
|
168
|
+
|
|
169
|
+
## Install
|
|
170
|
+
|
|
171
|
+
plextool is tested on Python 3.12. It declares Python 3.9 or later, so pip will
|
|
172
|
+
install it on older versions. If something doesn't work on one, please
|
|
173
|
+
[open an issue](https://github.com/milkmansson/plextool/issues) with your Python
|
|
174
|
+
version.
|
|
175
|
+
|
|
176
|
+
`plextool subs` also needs MKVToolNix (`mkvmerge`, `mkvextract`) for MKV files and
|
|
177
|
+
FFmpeg (`ffmpeg`, `ffprobe`) for MP4 files. You only need the ones for the file types
|
|
178
|
+
you use.
|
|
179
|
+
|
|
180
|
+
```console
|
|
181
|
+
$ pip install plextool # installs the `plextool` command
|
|
182
|
+
$ plextool --help
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
To install from a checkout instead (this needs `git`):
|
|
186
|
+
|
|
187
|
+
```console
|
|
188
|
+
$ git clone https://github.com/milkmansson/plextool.git
|
|
189
|
+
$ cd plextool
|
|
190
|
+
$ pip install .
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
To run from a checkout without installing, `pip install -r requirements.txt`
|
|
194
|
+
and use `python3 -m plextool ...` instead.
|
|
195
|
+
|
|
196
|
+
## Connecting
|
|
197
|
+
|
|
198
|
+
Sign in once. This prints a link (and a QR code, if `segno` is installed) to
|
|
199
|
+
approve in a browser:
|
|
200
|
+
|
|
201
|
+
```console
|
|
202
|
+
$ plextool auth login
|
|
203
|
+
$ plextool server find # skip if the server is on this machine
|
|
204
|
+
$ plextool server use "My Server" # or: server use http://myserver:32400
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Without a saved choice, plextool uses `http://localhost:32400`, or `$PLEX_URL`
|
|
208
|
+
or `--base-url` if given. [`plextool server`](https://plextool.readthedocs.io/en/latest/commands/server.html) explains the
|
|
209
|
+
options, including working with no internet or directly on the server's machine.
|
|
210
|
+
|
|
211
|
+
That saves a Plex token in `~/.config/plextool/` (readable only by you).
|
|
212
|
+
[`plextool auth`](https://plextool.readthedocs.io/en/latest/commands/auth.html) explains exactly what is kept, how to wipe it
|
|
213
|
+
with `plextool auth wipe`, and `--type jwt`, a short-lived token that plex.tv
|
|
214
|
+
accepts but Plex Media Server doesn't yet.
|
|
215
|
+
|
|
216
|
+
If you would rather supply your own [Plex token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/),
|
|
217
|
+
plextool looks for one in this order: `--token`, `--token-file FILE` (`-` for
|
|
218
|
+
standard input), `$PLEX_TOKEN`, the output of `$PLEX_TOKEN_CMD`, then the saved
|
|
219
|
+
login. Prefer `--token-file` or the environment to `--token`, which leaves the
|
|
220
|
+
token in your shell history.
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
```console
|
|
225
|
+
$ pip install -e '.[dev]'
|
|
226
|
+
$ pytest
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Disclaimer
|
|
230
|
+
|
|
231
|
+
These tools only use the documented plexapi calls, but they do write to your
|
|
232
|
+
server when you pass `--commit`. Check the dry run first. Use at your own risk.
|
plextool-3.0.1/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# plextool
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> Still in development/testing... please raise an issue if you find one!
|
|
5
|
+
|
|
6
|
+
Command line tools for Plex library maintenance, built on
|
|
7
|
+
[python-plexapi](https://python-plexapi.readthedocs.io/). They talk to the
|
|
8
|
+
server through its API. Nothing here touches the Plex database file, which is
|
|
9
|
+
too risky to edit by hand.
|
|
10
|
+
|
|
11
|
+
## What it is useful for
|
|
12
|
+
|
|
13
|
+
plextool is a command line toolkit for looking after a Plex Media Server: fixing
|
|
14
|
+
things Plex's web interface can't, and finding problems across a large library.
|
|
15
|
+
Typical jobs:
|
|
16
|
+
|
|
17
|
+
- **Fix the Recently Added order.** This is what it was first built for. Plex has no way to
|
|
18
|
+
edit the Recently Added lists, so after you rebuild the database, delete
|
|
19
|
+
duplicate files and rescan, or reorganise files on disk, everything shows up as
|
|
20
|
+
"new" and the real newest content is buried. [`plextool recent`](https://plextool.readthedocs.io/en/latest/commands/recent.html)
|
|
21
|
+
changes the "added at" date of movies, shows, seasons and episodes: set each
|
|
22
|
+
item back to its own release or air date, shift dates by an offset, or set one date for a
|
|
23
|
+
whole batch. The date is locked so a rescan can't undo it.
|
|
24
|
+
```console
|
|
25
|
+
$ plextool recent --recent 20 --release # preview: put the 20 newest back to their release dates
|
|
26
|
+
$ plextool recent --title "Some Show" --shift=-30d --commit
|
|
27
|
+
```
|
|
28
|
+
- **Find and remove duplicate movies and episodes.** [`plextool dupes`](https://plextool.readthedocs.io/en/latest/commands/dupes.html)
|
|
29
|
+
lists every item Plex holds more than one file for, and can delete the extras you don't want.
|
|
30
|
+
- **Free up disk space.** [`plextool stale`](https://plextool.readthedocs.io/en/latest/commands/stale.html) finds large, never-watched or
|
|
31
|
+
long-idle movies and shows.
|
|
32
|
+
- **Audit a library.** [`plextool audit`](https://plextool.readthedocs.io/en/latest/commands/audit.html) reports unmatched items, missing files,
|
|
33
|
+
missing posters or summaries, low-resolution video, and missing subtitle or audio languages.
|
|
34
|
+
- **Copy, back up and restore watched status.** [`plextool watched`](https://plextool.readthedocs.io/en/latest/commands/watched.html) moves watched
|
|
35
|
+
state between users (for example when a shared account is split), saves it to a file, and
|
|
36
|
+
restores it after a rebuild or when someone closes and reopens their Plex account.
|
|
37
|
+
- **Clear a stuck Continue Watching list.** [`plextool resume`](https://plextool.readthedocs.io/en/latest/commands/resume.html) lists and removes
|
|
38
|
+
entries that won't go away.
|
|
39
|
+
- **Back up and restore playlists.** [`plextool playlists`](https://plextool.readthedocs.io/en/latest/commands/playlists.html) saves video playlists
|
|
40
|
+
to a file and recreates them on the same or another server.
|
|
41
|
+
- **Get subtitles.** [`plextool subs`](https://plextool.readthedocs.io/en/latest/commands/subs.html) extracts the subtitle tracks inside MKV and MP4
|
|
42
|
+
files, searches Plex's subtitle providers for a language and downloads one, and, with
|
|
43
|
+
`--export`, copies the subtitle Plex downloaded out of its database into a file beside
|
|
44
|
+
the video so it works outside Plex too. Plex itself never writes that file.
|
|
45
|
+
- **Script Plex.** Reports can print JSON, and exports can go through a pipe, so the output
|
|
46
|
+
feeds other tools.
|
|
47
|
+
|
|
48
|
+
Everything that changes the server is a preview until you pass `--commit`. plextool uses Plex's API and
|
|
49
|
+
never edits the Plex database file. It signs in through your browser with
|
|
50
|
+
[`plextool auth`](https://plextool.readthedocs.io/en/latest/commands/auth.html), and works against a server on the same machine, across the
|
|
51
|
+
internet, or directly with no internet access ([`plextool server`](https://plextool.readthedocs.io/en/latest/commands/server.html)).
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command | What it does |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| [`plextool auth`](https://plextool.readthedocs.io/en/latest/commands/auth.html) | Sign in to Plex with a link or QR code, list and remove old sign-ins from your account, and wipe the saved login and keys. |
|
|
58
|
+
| [`plextool server`](https://plextool.readthedocs.io/en/latest/commands/server.html) | Find servers on your account or network and choose the one to use. Covers working offline or directly against the server. |
|
|
59
|
+
| [`plextool status`](https://plextool.readthedocs.io/en/latest/commands/status.html) | Server version, library sizes, who is watching what. |
|
|
60
|
+
| [`plextool scan`](https://plextool.readthedocs.io/en/latest/commands/scan.html) | Scan libraries or a single folder, refresh metadata, empty trash, optimize. |
|
|
61
|
+
| [`plextool recent`](https://plextool.readthedocs.io/en/latest/commands/recent.html) | Change "added at" dates to reorder Recently Added. Works on movies, shows, seasons and episodes. |
|
|
62
|
+
| [`plextool dupes`](https://plextool.readthedocs.io/en/latest/commands/dupes.html) | List movies and episodes that have more than one file, and optionally delete the extras. |
|
|
63
|
+
| [`plextool stale`](https://plextool.readthedocs.io/en/latest/commands/stale.html) | Find large, unwatched or long-idle content to free up space. |
|
|
64
|
+
| [`plextool audit`](https://plextool.readthedocs.io/en/latest/commands/audit.html) | Find unmatched items, missing files, missing posters or summaries, low resolution, missing subtitle or audio languages. |
|
|
65
|
+
| [`plextool watched`](https://plextool.readthedocs.io/en/latest/commands/watched.html) | Copy watched state between users, back it up and restore it, or mark a show or movie watched. |
|
|
66
|
+
| [`plextool resume`](https://plextool.readthedocs.io/en/latest/commands/resume.html) | List or clear stuck Continue Watching entries. |
|
|
67
|
+
| [`plextool playlists`](https://plextool.readthedocs.io/en/latest/commands/playlists.html) | Back up and restore video playlists. |
|
|
68
|
+
| [`plextool subs`](https://plextool.readthedocs.io/en/latest/commands/subs.html) | Extract subtitles from MKV and MP4 files, search Plex's subtitle providers for new ones and pick which to download, and copy subtitles Plex downloaded (kept in its database, not beside the video) out as files next to the video. Files are named for Plex, forced and SDH included. |
|
|
69
|
+
|
|
70
|
+
The `plextool` commands that change anything do a dry run unless you pass
|
|
71
|
+
`--commit`. Deleting files (`dupes --keep`) also asks you to type `DELETE`.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
Install it from PyPI. This needs `python` installed as a prerequisite:
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
$ python3 -m venv .venv && source .venv/bin/activate # optional: keeps plextool's packages separate
|
|
79
|
+
$ pip install 'plextool[qr]' # '[qr]' adds a QR code to the sign-in; leave it off if you don't want one
|
|
80
|
+
$ plextool auth login # open the link it prints (or scan the QR code) and approve
|
|
81
|
+
$ plextool server find # servers on your Plex account and on your network
|
|
82
|
+
$ plextool server use "My Server" # one of the names from the list
|
|
83
|
+
$ plextool status # libraries, and who is watching what right now
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`status` ends with what is playing:
|
|
87
|
+
|
|
88
|
+
```console
|
|
89
|
+
Now playing (1 session, 0 transcoding)
|
|
90
|
+
alice: Some Movie (2020) [playing] on Living Room TV
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To get, say, Swedish subtitles for that movie from the subtitle providers Plex searches:
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
$ plextool subs fetch -t "Some Movie" --lang sv --list # see what is available; nothing is downloaded
|
|
97
|
+
$ plextool subs fetch -t "Some Movie" --lang sv --choose --commit # pick one; Plex downloads it
|
|
98
|
+
$ plextool subs fetch -t "Some Movie" --lang sv --choose --export --commit # same, and also save a file beside the video
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Where the subtitle ends up.** When Plex downloads a subtitle from a provider, it keeps
|
|
102
|
+
the copy in its own database. It does not put a file next to the video, so only Plex can
|
|
103
|
+
show it: another player, or another Plex server, never sees it. The first two commands stop
|
|
104
|
+
there, and you pick the subtitle from the player's subtitle menu.
|
|
105
|
+
|
|
106
|
+
**`--export` gets around that.** Once Plex has downloaded the subtitle, plextool copies
|
|
107
|
+
Plex's stored copy out of the server and saves it as an ordinary file beside the video
|
|
108
|
+
(`<video name>.sv.srt`), which any player can use. It doesn't read the video file, so the
|
|
109
|
+
MKV or MP4 is never opened, and it doesn't contact the subtitle provider. (Subtitle
|
|
110
|
+
tracks that are already inside an MKV or MP4 are a different job, for `plextool subs extract`.)
|
|
111
|
+
For `--export`, the machine you run it on must be able to read the media files. If it sees
|
|
112
|
+
them under a different path than Plex does, use `--map`. The
|
|
113
|
+
[`subs` page](https://plextool.readthedocs.io/en/latest/commands/subs.html#where-each-subtitle-comes-from) sets out where each kind of subtitle comes from.
|
|
114
|
+
|
|
115
|
+
For a TV show, `-t "Some Show"` covers every episode, which is a search for each one,
|
|
116
|
+
so start with `--list`.
|
|
117
|
+
|
|
118
|
+
If the server is on the machine you are typing on, skip `server find` and `server use`:
|
|
119
|
+
plextool tries `http://localhost:32400` by default. Without internet access, see
|
|
120
|
+
[working offline](https://plextool.readthedocs.io/en/latest/commands/server.html#working-offline-or-directly-against-the-server).
|
|
121
|
+
|
|
122
|
+
## Getting help
|
|
123
|
+
|
|
124
|
+
Every command explains itself, with options and examples:
|
|
125
|
+
|
|
126
|
+
```console
|
|
127
|
+
$ plextool --help
|
|
128
|
+
$ plextool recent --help
|
|
129
|
+
$ plextool watched copy --help
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A fuller page for each command is on [Read the Docs](https://plextool.readthedocs.io/)
|
|
133
|
+
(linked in the table above); its source is in `docs/source/commands/`.
|
|
134
|
+
|
|
135
|
+
## Install
|
|
136
|
+
|
|
137
|
+
plextool is tested on Python 3.12. It declares Python 3.9 or later, so pip will
|
|
138
|
+
install it on older versions. If something doesn't work on one, please
|
|
139
|
+
[open an issue](https://github.com/milkmansson/plextool/issues) with your Python
|
|
140
|
+
version.
|
|
141
|
+
|
|
142
|
+
`plextool subs` also needs MKVToolNix (`mkvmerge`, `mkvextract`) for MKV files and
|
|
143
|
+
FFmpeg (`ffmpeg`, `ffprobe`) for MP4 files. You only need the ones for the file types
|
|
144
|
+
you use.
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
$ pip install plextool # installs the `plextool` command
|
|
148
|
+
$ plextool --help
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
To install from a checkout instead (this needs `git`):
|
|
152
|
+
|
|
153
|
+
```console
|
|
154
|
+
$ git clone https://github.com/milkmansson/plextool.git
|
|
155
|
+
$ cd plextool
|
|
156
|
+
$ pip install .
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To run from a checkout without installing, `pip install -r requirements.txt`
|
|
160
|
+
and use `python3 -m plextool ...` instead.
|
|
161
|
+
|
|
162
|
+
## Connecting
|
|
163
|
+
|
|
164
|
+
Sign in once. This prints a link (and a QR code, if `segno` is installed) to
|
|
165
|
+
approve in a browser:
|
|
166
|
+
|
|
167
|
+
```console
|
|
168
|
+
$ plextool auth login
|
|
169
|
+
$ plextool server find # skip if the server is on this machine
|
|
170
|
+
$ plextool server use "My Server" # or: server use http://myserver:32400
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Without a saved choice, plextool uses `http://localhost:32400`, or `$PLEX_URL`
|
|
174
|
+
or `--base-url` if given. [`plextool server`](https://plextool.readthedocs.io/en/latest/commands/server.html) explains the
|
|
175
|
+
options, including working with no internet or directly on the server's machine.
|
|
176
|
+
|
|
177
|
+
That saves a Plex token in `~/.config/plextool/` (readable only by you).
|
|
178
|
+
[`plextool auth`](https://plextool.readthedocs.io/en/latest/commands/auth.html) explains exactly what is kept, how to wipe it
|
|
179
|
+
with `plextool auth wipe`, and `--type jwt`, a short-lived token that plex.tv
|
|
180
|
+
accepts but Plex Media Server doesn't yet.
|
|
181
|
+
|
|
182
|
+
If you would rather supply your own [Plex token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/),
|
|
183
|
+
plextool looks for one in this order: `--token`, `--token-file FILE` (`-` for
|
|
184
|
+
standard input), `$PLEX_TOKEN`, the output of `$PLEX_TOKEN_CMD`, then the saved
|
|
185
|
+
login. Prefer `--token-file` or the environment to `--token`, which leaves the
|
|
186
|
+
token in your shell history.
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
```console
|
|
191
|
+
$ pip install -e '.[dev]'
|
|
192
|
+
$ pytest
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Disclaimer
|
|
196
|
+
|
|
197
|
+
These tools only use the documented plexapi calls, but they do write to your
|
|
198
|
+
server when you pass `--commit`. Check the dry run first. Use at your own risk.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""audit: find metadata and file problems across libraries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from .common import (
|
|
9
|
+
FILE_LEVEL,
|
|
10
|
+
PlexToolError,
|
|
11
|
+
add_command,
|
|
12
|
+
add_connection_args,
|
|
13
|
+
add_library_arg,
|
|
14
|
+
connect,
|
|
15
|
+
describe,
|
|
16
|
+
paint,
|
|
17
|
+
pick_sections,
|
|
18
|
+
plural,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
LOW_RES = {"sd", "480", "576", "360", "240"}
|
|
22
|
+
UNMATCHED_PREFIXES = ("local://", "com.plexapp.agents.none")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Each check takes (item, lang) and returns a short reason string, or None if the item is fine.
|
|
26
|
+
|
|
27
|
+
def check_unmatched(item, lang):
|
|
28
|
+
if str(item.guid).startswith(UNMATCHED_PREFIXES):
|
|
29
|
+
return "not matched to an online source"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_no_poster(item, lang):
|
|
33
|
+
if not item.thumb:
|
|
34
|
+
return "no poster"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_no_summary(item, lang):
|
|
38
|
+
if not item.summary:
|
|
39
|
+
return "no summary"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_missing_files(item, lang):
|
|
43
|
+
bad = [part.file for media in item.media for part in media.parts if part.exists is False]
|
|
44
|
+
if bad:
|
|
45
|
+
return f"file missing on disk: {bad[0]}" + (f" (+{len(bad) - 1} more)" if len(bad) > 1 else "")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_low_res(item, lang):
|
|
49
|
+
resolutions = {m.videoResolution for m in item.media if m.videoResolution}
|
|
50
|
+
if resolutions and resolutions <= LOW_RES:
|
|
51
|
+
return f"low resolution ({', '.join(sorted(resolutions))})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _streams(item, kind):
|
|
55
|
+
return [s for media in item.media for part in media.parts for s in getattr(part, kind)()]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_no_subs(item, lang):
|
|
59
|
+
if not any(s.languageCode == lang for s in _streams(item, "subtitleStreams")):
|
|
60
|
+
return f"no '{lang}' subtitles"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_no_audio(item, lang):
|
|
64
|
+
if not any(s.languageCode == lang for s in _streams(item, "audioStreams")):
|
|
65
|
+
return f"no '{lang}' audio"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# name -> (function, works on files (movie/episode) rather than the library's top level, needs --lang,
|
|
69
|
+
# needs the full item loaded to see its streams)
|
|
70
|
+
CHECKS = {
|
|
71
|
+
"unmatched": (check_unmatched, False, False, False),
|
|
72
|
+
"no-poster": (check_no_poster, False, False, False),
|
|
73
|
+
"no-summary": (check_no_summary, False, False, False),
|
|
74
|
+
"missing-files": (check_missing_files, True, False, False),
|
|
75
|
+
"low-res": (check_low_res, True, False, False),
|
|
76
|
+
"no-subs": (check_no_subs, True, True, True),
|
|
77
|
+
"no-audio": (check_no_audio, True, True, True),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_check(name: str, items: list, lang: str | None) -> list[dict]:
|
|
82
|
+
"""Apply one check to items; returns [{'item': description, 'reason': ...}]."""
|
|
83
|
+
func, _, _, needs_full = CHECKS[name]
|
|
84
|
+
found = []
|
|
85
|
+
for item in items:
|
|
86
|
+
if needs_full:
|
|
87
|
+
item.reload()
|
|
88
|
+
reason = func(item, lang)
|
|
89
|
+
if reason:
|
|
90
|
+
found.append({"item": describe(item), "reason": reason})
|
|
91
|
+
return found
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def add_parser(subparsers) -> None:
|
|
95
|
+
parser = add_command(
|
|
96
|
+
subparsers, 'audit',
|
|
97
|
+
'find unmatched items, missing files, missing subtitles and more',
|
|
98
|
+
"Report problems in your libraries. Read-only. Checks: unmatched, no-poster, no-summary, missing-files, low-res, no-subs, no-audio. With no --check, every check that doesn't need --lang runs. no-subs and no-audio read every item's tracks and are slow on large libraries.",
|
|
99
|
+
['plextool audit',
|
|
100
|
+
'plextool audit -c missing-files -c unmatched',
|
|
101
|
+
"plextool audit -c no-subs --lang eng -l 'TV Shows'"])
|
|
102
|
+
add_connection_args(parser)
|
|
103
|
+
add_library_arg(parser)
|
|
104
|
+
parser.add_argument("-c", "--check", action="append", choices=list(CHECKS), metavar="CHECK",
|
|
105
|
+
help="check to run; repeat for several (default: all that don't need --lang)")
|
|
106
|
+
parser.add_argument("--lang", help="3-letter language code, as Plex shows it, for no-subs / no-audio, eg eng")
|
|
107
|
+
parser.add_argument("-n", "--limit", type=int, metavar="N", help="show at most N problems per check")
|
|
108
|
+
parser.add_argument("--json", action="store_true", help="print JSON instead of text")
|
|
109
|
+
parser.set_defaults(run=run)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run(args: argparse.Namespace) -> int:
|
|
113
|
+
names = args.check or [n for n, (_, _, needs_lang, _) in CHECKS.items() if not needs_lang]
|
|
114
|
+
for name in names:
|
|
115
|
+
if CHECKS[name][2] and not args.lang:
|
|
116
|
+
raise PlexToolError(f"Check '{name}' needs --lang, eg --lang eng")
|
|
117
|
+
if args.limit is not None and args.limit < 1:
|
|
118
|
+
raise PlexToolError("--limit must be at least 1")
|
|
119
|
+
|
|
120
|
+
plex = connect(args)
|
|
121
|
+
report: dict = {}
|
|
122
|
+
for section in pick_sections(plex, args.library):
|
|
123
|
+
cache: dict[str, list] = {}
|
|
124
|
+
for name in names:
|
|
125
|
+
level = FILE_LEVEL[section.TYPE] if CHECKS[name][1] else section.TYPE
|
|
126
|
+
if level not in cache:
|
|
127
|
+
cache[level] = section.search(libtype=level)
|
|
128
|
+
report.setdefault(name, {})[section.title] = run_check(name, cache[level], args.lang)
|
|
129
|
+
|
|
130
|
+
if args.json:
|
|
131
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
total = 0
|
|
135
|
+
for name, libs in report.items():
|
|
136
|
+
for title, problems in libs.items():
|
|
137
|
+
print(paint(f"\n== {name} in {title}: {plural(len(problems), 'problem')} ==", "cyan"))
|
|
138
|
+
for problem in problems[:args.limit]:
|
|
139
|
+
print(f" {problem['item']}: {problem['reason']}")
|
|
140
|
+
total += len(problems)
|
|
141
|
+
print(f"\n{plural(total, 'problem')} found.")
|
|
142
|
+
return 0
|