stashapp-cli 0.1.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.
- stashapp_cli-0.1.0/LICENSE +21 -0
- stashapp_cli-0.1.0/PKG-INFO +208 -0
- stashapp_cli-0.1.0/README.md +184 -0
- stashapp_cli-0.1.0/pyproject.toml +56 -0
- stashapp_cli-0.1.0/pyproject.toml.orig +51 -0
- stashapp_cli-0.1.0/src/stashapp_cli/__init__.py +2 -0
- stashapp_cli-0.1.0/src/stashapp_cli/cli.py +149 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/__init__.py +39 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/_common.py +440 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/backup.py +70 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/generate.py +88 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/graphql.py +212 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/group.py +137 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/introspect.py +35 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/job.py +122 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/performer.py +368 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/scan.py +92 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/scene.py +405 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/scrape.py +133 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/stashbox.py +173 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/studio.py +227 -0
- stashapp_cli-0.1.0/src/stashapp_cli/commands/tag.py +694 -0
- stashapp_cli-0.1.0/src/stashapp_cli/core.py +165 -0
- stashapp_cli-0.1.0/src/stashapp_cli/images.py +72 -0
- stashapp_cli-0.1.0/src/stashapp_cli/introspect.py +343 -0
- stashapp_cli-0.1.0/src/stashapp_cli/py.typed +0 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/__init__.py +40 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/_get.py +27 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/_image_flags.py +30 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/backup.py +20 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/group.py +117 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/job.py +29 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/metadata.py +42 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/performer.py +179 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/scene.py +139 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/scrape.py +183 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/sources.py +76 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/stashbox.py +67 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/studio.py +145 -0
- stashapp_cli-0.1.0/src/stashapp_cli/queries/tag.py +244 -0
- stashapp_cli-0.1.0/src/stashapp_cli/sources.py +116 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 razputingh
|
|
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,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stashapp-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line client for the Stash GraphQL API
|
|
5
|
+
Keywords: stash,graphql,cli
|
|
6
|
+
Author: razputingh
|
|
7
|
+
Author-email: razputingh <331436478+razputingh@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Project-URL: Homepage, https://github.com/razputingh/stashapp-cli
|
|
20
|
+
Project-URL: Documentation, https://github.com/razputingh/stashapp-cli/tree/main/docs
|
|
21
|
+
Project-URL: Repository, https://github.com/razputingh/stashapp-cli
|
|
22
|
+
Project-URL: Issues, https://github.com/razputingh/stashapp-cli/issues
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# stashapp-cli
|
|
26
|
+
|
|
27
|
+
stashapp-cli is a command-line tool for [Stash](https://github.com/stashapp/stash),
|
|
28
|
+
the self-hosted organizer for your media library. It reads and changes your
|
|
29
|
+
Stash library through the GraphQL API of Stash. Each command prints JSON, so
|
|
30
|
+
you can use stashapp-cli in shell scripts and with tools such as `jq`.
|
|
31
|
+
|
|
32
|
+
stashapp-cli is not part of the Stash project. The Stash developers do not make
|
|
33
|
+
or support it.
|
|
34
|
+
|
|
35
|
+
## What it does
|
|
36
|
+
|
|
37
|
+
- Find, create, and update studios, tags, groups, scenes, and performers.
|
|
38
|
+
- Merge duplicate tags and performers, and delete tags. A dry run first
|
|
39
|
+
reports what the change will remove.
|
|
40
|
+
- Scrape metadata from stash-boxes and installed scrapers.
|
|
41
|
+
- Start scan, generate, and identify jobs, and wait for them to end.
|
|
42
|
+
- Back up the Stash database, and submit scene fingerprints to a stash-box.
|
|
43
|
+
- Send a GraphQL query or mutation that you write.
|
|
44
|
+
|
|
45
|
+
A stash-box is a shared database of scene and performer metadata, such as
|
|
46
|
+
StashDB.
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- Python `3.13` or later.
|
|
51
|
+
- A Stash server and an API key for it. stashapp-cli is tested with Stash `v0.31.1`.
|
|
52
|
+
- Optional: [jq](https://jqlang.org/), to read the JSON output in scripts. The
|
|
53
|
+
examples in the guides use it.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
Install stashapp-cli from PyPI with [uv](https://docs.astral.sh/uv/) or
|
|
58
|
+
[pipx](https://pipx.pypa.io/). The package is named `stashapp-cli`. The
|
|
59
|
+
command it installs is `stashapp-cli`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv tool install stashapp-cli
|
|
63
|
+
pipx install stashapp-cli
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To install the latest code from GitHub, run:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv tool install git+https://github.com/razputingh/stashapp-cli
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Connect to Stash
|
|
73
|
+
|
|
74
|
+
stashapp-cli reads two environment variables. It has no credential flags or
|
|
75
|
+
credential files.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export STASH_URL=http://localhost:9999
|
|
79
|
+
export STASH_API_KEY=<key>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`STASH_URL` is the base URL of your Stash server. You make an API key in Stash
|
|
83
|
+
under Settings > Security > Authentication. If a variable is not set, the
|
|
84
|
+
command stops with exit status 4. For the other exit statuses, read
|
|
85
|
+
[Errors and exit statuses][errors].
|
|
86
|
+
|
|
87
|
+
## Quick start
|
|
88
|
+
|
|
89
|
+
Make sure that your Stash server has every name that stashapp-cli uses. Run this
|
|
90
|
+
command before the first write to a server, and after you upgrade Stash:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
stashapp-cli introspect
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Find a tag by its name or alias:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
stashapp-cli tag find --name hd
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Make the tag if it does not exist. Without `--apply`, the command only reports
|
|
103
|
+
what it would send:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
stashapp-cli tag find-or-create hd
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{"applied": false, "operation": "tagCreate", "input": {"name": "hd"}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Add `--apply` to make the change:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
stashapp-cli tag find-or-create hd --apply
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each command has a `--help` output with its options and examples:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
stashapp-cli --help
|
|
123
|
+
stashapp-cli tag --help
|
|
124
|
+
stashapp-cli tag merge --help
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Changes are dry runs by default
|
|
128
|
+
|
|
129
|
+
Each command has one of three kinds. The `--help` output of a command states
|
|
130
|
+
its kind.
|
|
131
|
+
|
|
132
|
+
| Kind | What the command does |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `read` | It never changes Stash. |
|
|
135
|
+
| `write` | It changes Stash only with `--apply`. Else it reports the change. |
|
|
136
|
+
| `destructive` | It can make a change that you cannot undo. |
|
|
137
|
+
|
|
138
|
+
A destructive change needs both `--apply` and `--force`. You can put `--apply`
|
|
139
|
+
anywhere in the command. Put `--force` after the command name, for example
|
|
140
|
+
`stashapp-cli tag merge --destination 3 --source 7 --apply --force`.
|
|
141
|
+
|
|
142
|
+
Before you merge or delete, back up the Stash database with
|
|
143
|
+
`stashapp-cli backup --apply`.
|
|
144
|
+
|
|
145
|
+
Do not use `--input` to add one tag, URL, or alias. A list in `--input`
|
|
146
|
+
replaces the whole list in Stash, so the other values are removed. Use the add
|
|
147
|
+
and remove options instead, for example
|
|
148
|
+
`stashapp-cli scene update 812 --add-tag-id 55`.
|
|
149
|
+
|
|
150
|
+
For the full rules, read [Writes are dry runs by default][writes] and
|
|
151
|
+
[Updates replace lists][lists].
|
|
152
|
+
|
|
153
|
+
## Commands
|
|
154
|
+
|
|
155
|
+
Each command is a group followed by an action, for example `tag merge`. The
|
|
156
|
+
[command reference][commands] has a table of all commands and their kinds.
|
|
157
|
+
|
|
158
|
+
| Group | What it does |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| [`studio`][studio-cmd] | Get, find, create, and update studios. |
|
|
161
|
+
| [`tag`][tag-cmd] | Get, find, create, update, delete, and merge tags. |
|
|
162
|
+
| [`group`][group-cmd] | Get, find, create, and update groups, for example films. |
|
|
163
|
+
| [`scene`][scene-cmd] | Get, find, and update scenes, and start identify jobs. |
|
|
164
|
+
| [`performer`][performer-cmd] | Get, find, create, update, and merge performers. |
|
|
165
|
+
| [`scrape`][scrape-cmd] | Get metadata from stash-boxes, scrapers, and URLs. |
|
|
166
|
+
| [`scan`, `generate`, `job`][jobs-cmd] | Start scan and generate jobs, and wait for jobs. |
|
|
167
|
+
| [`backup`][backup-cmd] | Back up the Stash database. |
|
|
168
|
+
| [`stashbox`][stashbox-cmd] | Submit scene fingerprints to a stash-box. |
|
|
169
|
+
| [`introspect`, `graphql`][graphql-cmd] | Check the schema, and send your own GraphQL. |
|
|
170
|
+
|
|
171
|
+
## Documentation
|
|
172
|
+
|
|
173
|
+
- [Usage][usage]: global options, input values, find filters, dry runs, and
|
|
174
|
+
list updates.
|
|
175
|
+
- [Command reference][commands]: the behavior of each command.
|
|
176
|
+
- [Guides][guides]: step-by-step tasks, such as how to merge duplicate tags.
|
|
177
|
+
- [Errors and exit statuses][errors]: the JSON error object, the error codes,
|
|
178
|
+
and the exit statuses.
|
|
179
|
+
- [Glossary][glossary]: the terms that these documents use.
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
Bug reports and pull requests are welcome. Read [CONTRIBUTING.md][contributing]
|
|
184
|
+
for how to run the tests and how to add a command.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
stashapp-cli is released under the [MIT License][license].
|
|
189
|
+
|
|
190
|
+
[writes]: docs/usage.md#writes-are-dry-runs-by-default
|
|
191
|
+
[lists]: docs/usage.md#updates-replace-lists
|
|
192
|
+
[commands]: docs/commands/README.md
|
|
193
|
+
[studio-cmd]: docs/commands/studio.md
|
|
194
|
+
[tag-cmd]: docs/commands/tag.md
|
|
195
|
+
[group-cmd]: docs/commands/group.md
|
|
196
|
+
[scene-cmd]: docs/commands/scene.md
|
|
197
|
+
[performer-cmd]: docs/commands/performer.md
|
|
198
|
+
[scrape-cmd]: docs/commands/scrape.md
|
|
199
|
+
[jobs-cmd]: docs/commands/jobs.md
|
|
200
|
+
[backup-cmd]: docs/commands/backup.md
|
|
201
|
+
[stashbox-cmd]: docs/commands/stashbox.md
|
|
202
|
+
[graphql-cmd]: docs/commands/graphql.md
|
|
203
|
+
[usage]: docs/usage.md
|
|
204
|
+
[guides]: docs/guides/README.md
|
|
205
|
+
[errors]: docs/errors.md
|
|
206
|
+
[glossary]: docs/glossary.md
|
|
207
|
+
[contributing]: CONTRIBUTING.md
|
|
208
|
+
[license]: LICENSE
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# stashapp-cli
|
|
2
|
+
|
|
3
|
+
stashapp-cli is a command-line tool for [Stash](https://github.com/stashapp/stash),
|
|
4
|
+
the self-hosted organizer for your media library. It reads and changes your
|
|
5
|
+
Stash library through the GraphQL API of Stash. Each command prints JSON, so
|
|
6
|
+
you can use stashapp-cli in shell scripts and with tools such as `jq`.
|
|
7
|
+
|
|
8
|
+
stashapp-cli is not part of the Stash project. The Stash developers do not make
|
|
9
|
+
or support it.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- Find, create, and update studios, tags, groups, scenes, and performers.
|
|
14
|
+
- Merge duplicate tags and performers, and delete tags. A dry run first
|
|
15
|
+
reports what the change will remove.
|
|
16
|
+
- Scrape metadata from stash-boxes and installed scrapers.
|
|
17
|
+
- Start scan, generate, and identify jobs, and wait for them to end.
|
|
18
|
+
- Back up the Stash database, and submit scene fingerprints to a stash-box.
|
|
19
|
+
- Send a GraphQL query or mutation that you write.
|
|
20
|
+
|
|
21
|
+
A stash-box is a shared database of scene and performer metadata, such as
|
|
22
|
+
StashDB.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Python `3.13` or later.
|
|
27
|
+
- A Stash server and an API key for it. stashapp-cli is tested with Stash `v0.31.1`.
|
|
28
|
+
- Optional: [jq](https://jqlang.org/), to read the JSON output in scripts. The
|
|
29
|
+
examples in the guides use it.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Install stashapp-cli from PyPI with [uv](https://docs.astral.sh/uv/) or
|
|
34
|
+
[pipx](https://pipx.pypa.io/). The package is named `stashapp-cli`. The
|
|
35
|
+
command it installs is `stashapp-cli`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv tool install stashapp-cli
|
|
39
|
+
pipx install stashapp-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To install the latest code from GitHub, run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv tool install git+https://github.com/razputingh/stashapp-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Connect to Stash
|
|
49
|
+
|
|
50
|
+
stashapp-cli reads two environment variables. It has no credential flags or
|
|
51
|
+
credential files.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export STASH_URL=http://localhost:9999
|
|
55
|
+
export STASH_API_KEY=<key>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`STASH_URL` is the base URL of your Stash server. You make an API key in Stash
|
|
59
|
+
under Settings > Security > Authentication. If a variable is not set, the
|
|
60
|
+
command stops with exit status 4. For the other exit statuses, read
|
|
61
|
+
[Errors and exit statuses][errors].
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
Make sure that your Stash server has every name that stashapp-cli uses. Run this
|
|
66
|
+
command before the first write to a server, and after you upgrade Stash:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
stashapp-cli introspect
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Find a tag by its name or alias:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
stashapp-cli tag find --name hd
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Make the tag if it does not exist. Without `--apply`, the command only reports
|
|
79
|
+
what it would send:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
stashapp-cli tag find-or-create hd
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{"applied": false, "operation": "tagCreate", "input": {"name": "hd"}}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Add `--apply` to make the change:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
stashapp-cli tag find-or-create hd --apply
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Each command has a `--help` output with its options and examples:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
stashapp-cli --help
|
|
99
|
+
stashapp-cli tag --help
|
|
100
|
+
stashapp-cli tag merge --help
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Changes are dry runs by default
|
|
104
|
+
|
|
105
|
+
Each command has one of three kinds. The `--help` output of a command states
|
|
106
|
+
its kind.
|
|
107
|
+
|
|
108
|
+
| Kind | What the command does |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `read` | It never changes Stash. |
|
|
111
|
+
| `write` | It changes Stash only with `--apply`. Else it reports the change. |
|
|
112
|
+
| `destructive` | It can make a change that you cannot undo. |
|
|
113
|
+
|
|
114
|
+
A destructive change needs both `--apply` and `--force`. You can put `--apply`
|
|
115
|
+
anywhere in the command. Put `--force` after the command name, for example
|
|
116
|
+
`stashapp-cli tag merge --destination 3 --source 7 --apply --force`.
|
|
117
|
+
|
|
118
|
+
Before you merge or delete, back up the Stash database with
|
|
119
|
+
`stashapp-cli backup --apply`.
|
|
120
|
+
|
|
121
|
+
Do not use `--input` to add one tag, URL, or alias. A list in `--input`
|
|
122
|
+
replaces the whole list in Stash, so the other values are removed. Use the add
|
|
123
|
+
and remove options instead, for example
|
|
124
|
+
`stashapp-cli scene update 812 --add-tag-id 55`.
|
|
125
|
+
|
|
126
|
+
For the full rules, read [Writes are dry runs by default][writes] and
|
|
127
|
+
[Updates replace lists][lists].
|
|
128
|
+
|
|
129
|
+
## Commands
|
|
130
|
+
|
|
131
|
+
Each command is a group followed by an action, for example `tag merge`. The
|
|
132
|
+
[command reference][commands] has a table of all commands and their kinds.
|
|
133
|
+
|
|
134
|
+
| Group | What it does |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| [`studio`][studio-cmd] | Get, find, create, and update studios. |
|
|
137
|
+
| [`tag`][tag-cmd] | Get, find, create, update, delete, and merge tags. |
|
|
138
|
+
| [`group`][group-cmd] | Get, find, create, and update groups, for example films. |
|
|
139
|
+
| [`scene`][scene-cmd] | Get, find, and update scenes, and start identify jobs. |
|
|
140
|
+
| [`performer`][performer-cmd] | Get, find, create, update, and merge performers. |
|
|
141
|
+
| [`scrape`][scrape-cmd] | Get metadata from stash-boxes, scrapers, and URLs. |
|
|
142
|
+
| [`scan`, `generate`, `job`][jobs-cmd] | Start scan and generate jobs, and wait for jobs. |
|
|
143
|
+
| [`backup`][backup-cmd] | Back up the Stash database. |
|
|
144
|
+
| [`stashbox`][stashbox-cmd] | Submit scene fingerprints to a stash-box. |
|
|
145
|
+
| [`introspect`, `graphql`][graphql-cmd] | Check the schema, and send your own GraphQL. |
|
|
146
|
+
|
|
147
|
+
## Documentation
|
|
148
|
+
|
|
149
|
+
- [Usage][usage]: global options, input values, find filters, dry runs, and
|
|
150
|
+
list updates.
|
|
151
|
+
- [Command reference][commands]: the behavior of each command.
|
|
152
|
+
- [Guides][guides]: step-by-step tasks, such as how to merge duplicate tags.
|
|
153
|
+
- [Errors and exit statuses][errors]: the JSON error object, the error codes,
|
|
154
|
+
and the exit statuses.
|
|
155
|
+
- [Glossary][glossary]: the terms that these documents use.
|
|
156
|
+
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
Bug reports and pull requests are welcome. Read [CONTRIBUTING.md][contributing]
|
|
160
|
+
for how to run the tests and how to add a command.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
stashapp-cli is released under the [MIT License][license].
|
|
165
|
+
|
|
166
|
+
[writes]: docs/usage.md#writes-are-dry-runs-by-default
|
|
167
|
+
[lists]: docs/usage.md#updates-replace-lists
|
|
168
|
+
[commands]: docs/commands/README.md
|
|
169
|
+
[studio-cmd]: docs/commands/studio.md
|
|
170
|
+
[tag-cmd]: docs/commands/tag.md
|
|
171
|
+
[group-cmd]: docs/commands/group.md
|
|
172
|
+
[scene-cmd]: docs/commands/scene.md
|
|
173
|
+
[performer-cmd]: docs/commands/performer.md
|
|
174
|
+
[scrape-cmd]: docs/commands/scrape.md
|
|
175
|
+
[jobs-cmd]: docs/commands/jobs.md
|
|
176
|
+
[backup-cmd]: docs/commands/backup.md
|
|
177
|
+
[stashbox-cmd]: docs/commands/stashbox.md
|
|
178
|
+
[graphql-cmd]: docs/commands/graphql.md
|
|
179
|
+
[usage]: docs/usage.md
|
|
180
|
+
[guides]: docs/guides/README.md
|
|
181
|
+
[errors]: docs/errors.md
|
|
182
|
+
[glossary]: docs/glossary.md
|
|
183
|
+
[contributing]: CONTRIBUTING.md
|
|
184
|
+
[license]: LICENSE
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stashapp-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Command-line client for the Stash GraphQL API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"stash",
|
|
10
|
+
"graphql",
|
|
11
|
+
"cli",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.13"
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "razputingh"
|
|
28
|
+
email = "331436478+razputingh@users.noreply.github.com"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/razputingh/stashapp-cli"
|
|
32
|
+
Documentation = "https://github.com/razputingh/stashapp-cli/tree/main/docs"
|
|
33
|
+
Repository = "https://github.com/razputingh/stashapp-cli"
|
|
34
|
+
Issues = "https://github.com/razputingh/stashapp-cli/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
stashapp-cli = "stashapp_cli.cli:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.11.16,<0.12.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=9.1.1",
|
|
46
|
+
"ruff>=0.16.9",
|
|
47
|
+
"ty>=0.0.84",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
target-version = "py313"
|
|
55
|
+
src = ["src"]
|
|
56
|
+
exclude = ["*.md"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stashapp-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Command-line client for the Stash GraphQL API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "razputingh", email = "331436478+razputingh@users.noreply.github.com" }
|
|
10
|
+
]
|
|
11
|
+
keywords = ["stash", "graphql", "cli"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.13"
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/razputingh/stashapp-cli"
|
|
27
|
+
Documentation = "https://github.com/razputingh/stashapp-cli/tree/main/docs"
|
|
28
|
+
Repository = "https://github.com/razputingh/stashapp-cli"
|
|
29
|
+
Issues = "https://github.com/razputingh/stashapp-cli/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
stashapp-cli = "stashapp_cli.cli:main"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["uv_build>=0.11.16,<0.12.0"]
|
|
36
|
+
build-backend = "uv_build"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=9.1.1",
|
|
41
|
+
"ruff>=0.16.9",
|
|
42
|
+
"ty>=0.0.84",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py313"
|
|
50
|
+
src = ["src"]
|
|
51
|
+
exclude = ["*.md"]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""stashapp-cli: inspect and modify Stash's GraphQL entities directly.
|
|
2
|
+
|
|
3
|
+
Layers: a raw `graphql` escape hatch, generic per-resource `find`/`get`/
|
|
4
|
+
`create`/`update` commands, and named platform-quirk commands (`tag
|
|
5
|
+
find-or-create`, `scrape group-url`/`scene-url`). Every mutating command is
|
|
6
|
+
gated by the global `--apply` flag (dry-run by default); destructive commands
|
|
7
|
+
(`performer merge`, and raw GraphQL *Destroy/*Delete/*Merge, ...) additionally
|
|
8
|
+
require `--force`. Results print as JSON on stdout; human-facing status and
|
|
9
|
+
errors print to stderr.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import importlib.metadata
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from . import commands, core
|
|
21
|
+
from .commands._common import add_examples, positive_seconds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _package_version() -> str:
|
|
25
|
+
"""Return installed metadata, with a source-tree fallback for development."""
|
|
26
|
+
try:
|
|
27
|
+
return importlib.metadata.version("stashapp-cli")
|
|
28
|
+
except importlib.metadata.PackageNotFoundError:
|
|
29
|
+
pyproject = Path(__file__).parents[2] / "pyproject.toml"
|
|
30
|
+
try:
|
|
31
|
+
import tomllib
|
|
32
|
+
|
|
33
|
+
return tomllib.loads(pyproject.read_text())["project"]["version"]
|
|
34
|
+
except (OSError, KeyError, tomllib.TOMLDecodeError):
|
|
35
|
+
return "unknown"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _emit_error(error: core.CliError, json_errors: bool) -> None:
|
|
39
|
+
if json_errors:
|
|
40
|
+
core.output(
|
|
41
|
+
{"error": {"code": error.code, "message": str(error), **error.details}}
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
print(str(error), file=sys.stderr)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_apply(argv: list[str]) -> list[str]:
|
|
48
|
+
"""Normalize global arguments that argparse cannot otherwise disambiguate."""
|
|
49
|
+
normalized = list(argv)
|
|
50
|
+
for index, value in enumerate(normalized[:-1]):
|
|
51
|
+
if value == "--timeout" and normalized[index + 1].lower() in {
|
|
52
|
+
"-inf",
|
|
53
|
+
"-infinity",
|
|
54
|
+
}:
|
|
55
|
+
normalized[index : index + 2] = [f"--timeout={normalized[index + 1]}"]
|
|
56
|
+
break
|
|
57
|
+
if normalized.count("--apply") != 1:
|
|
58
|
+
return normalized
|
|
59
|
+
index = normalized.index("--apply")
|
|
60
|
+
if index == 0:
|
|
61
|
+
return normalized
|
|
62
|
+
return ["--apply", *normalized[:index], *normalized[index + 1 :]]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# --- entrypoint ------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def iter_leaf_commands(
|
|
69
|
+
parser: argparse.ArgumentParser, path: tuple[str, ...] = ()
|
|
70
|
+
) -> Iterator[tuple[tuple[str, ...], argparse.ArgumentParser]]:
|
|
71
|
+
"""Yield `(path, parser)` for every leaf command below `parser`."""
|
|
72
|
+
subparsers = [
|
|
73
|
+
action
|
|
74
|
+
for action in parser._actions
|
|
75
|
+
if isinstance(action, argparse._SubParsersAction)
|
|
76
|
+
]
|
|
77
|
+
if not subparsers:
|
|
78
|
+
yield path, parser
|
|
79
|
+
return
|
|
80
|
+
for action in subparsers:
|
|
81
|
+
for name, child in action.choices.items():
|
|
82
|
+
yield from iter_leaf_commands(child, (*path, name))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
86
|
+
parser = argparse.ArgumentParser(prog="stashapp-cli", description=__doc__)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--apply",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help=(
|
|
91
|
+
"Perform mutating commands instead of reporting a dry run "
|
|
92
|
+
"(default: dry-run; inert on read-only commands)."
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--json-errors",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help="Write expected failures as one JSON error envelope on stdout.",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--timeout",
|
|
102
|
+
type=positive_seconds,
|
|
103
|
+
default=core.DEFAULT_TIMEOUT,
|
|
104
|
+
metavar="SECONDS",
|
|
105
|
+
help="Maximum seconds for each Stash request (default: 30).",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument("--version", action="version", version=_package_version())
|
|
108
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
109
|
+
for module in commands.COMMAND_MODULES:
|
|
110
|
+
module.register(subparsers)
|
|
111
|
+
add_examples(
|
|
112
|
+
parser,
|
|
113
|
+
"export STASH_URL=http://localhost:9999; export STASH_API_KEY",
|
|
114
|
+
"stashapp-cli studio find --name ExampleStudio",
|
|
115
|
+
'stashapp-cli group create --input \'{"name": "Film"}\'',
|
|
116
|
+
"stashapp-cli group create --input @group.json --apply",
|
|
117
|
+
"stashapp-cli --json-errors --timeout 5 scene get 1",
|
|
118
|
+
)
|
|
119
|
+
parser.epilog = (parser.epilog or "") + (
|
|
120
|
+
"\n\nExit statuses: 0 success; 1 runtime or GraphQL failure; "
|
|
121
|
+
"2 usage or invalid input (including destructive_mutation: a destructive\n"
|
|
122
|
+
"command sent with --apply but without --force); "
|
|
123
|
+
"3 Stash endpoint returned HTTP 404; "
|
|
124
|
+
"4 missing credential or authentication failure."
|
|
125
|
+
)
|
|
126
|
+
return parser
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main(argv: list[str] | None = None) -> None:
|
|
130
|
+
parser = build_parser()
|
|
131
|
+
args = parser.parse_args(
|
|
132
|
+
_normalize_apply(list(argv))
|
|
133
|
+
if argv is not None
|
|
134
|
+
else _normalize_apply(sys.argv[1:])
|
|
135
|
+
)
|
|
136
|
+
try:
|
|
137
|
+
with core.request_timeout(args.timeout):
|
|
138
|
+
result = args.func(args)
|
|
139
|
+
except core.CliError as error:
|
|
140
|
+
_emit_error(error, args.json_errors)
|
|
141
|
+
sys.exit(error.exit_code)
|
|
142
|
+
except Exception as error: # noqa: BLE001
|
|
143
|
+
_emit_error(core.CliError(f"Unexpected error: {error}", 1), args.json_errors)
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
core.output(result)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|