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 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.
@@ -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.
@@ -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,3 @@
1
+ """plextool: maintenance helpers for a Plex Media Server, built on python-plexapi."""
2
+
3
+ __version__ = "3.0.1"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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