gog-cli 0.2.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.
- gog_cli-0.2.1/LICENSE +21 -0
- gog_cli-0.2.1/PKG-INFO +193 -0
- gog_cli-0.2.1/README.md +153 -0
- gog_cli-0.2.1/pyproject.toml +49 -0
- gog_cli-0.2.1/setup.cfg +4 -0
- gog_cli-0.2.1/src/gog_cli/__init__.py +5 -0
- gog_cli-0.2.1/src/gog_cli/api.py +143 -0
- gog_cli-0.2.1/src/gog_cli/aria2c.py +136 -0
- gog_cli-0.2.1/src/gog_cli/auth.py +217 -0
- gog_cli-0.2.1/src/gog_cli/backup.py +197 -0
- gog_cli-0.2.1/src/gog_cli/cli.py +550 -0
- gog_cli-0.2.1/src/gog_cli/config.py +120 -0
- gog_cli-0.2.1/src/gog_cli/downloader.py +196 -0
- gog_cli-0.2.1/src/gog_cli/errors.py +54 -0
- gog_cli-0.2.1/src/gog_cli/execution.py +1054 -0
- gog_cli-0.2.1/src/gog_cli/layout.py +72 -0
- gog_cli-0.2.1/src/gog_cli/listing.py +668 -0
- gog_cli-0.2.1/src/gog_cli/log.py +19 -0
- gog_cli-0.2.1/src/gog_cli/metadata.py +212 -0
- gog_cli-0.2.1/src/gog_cli/output.py +99 -0
- gog_cli-0.2.1/src/gog_cli/prompt.py +57 -0
- gog_cli-0.2.1/src/gog_cli/refresh.py +231 -0
- gog_cli-0.2.1/src/gog_cli/state.py +193 -0
- gog_cli-0.2.1/src/gog_cli/sync.py +146 -0
- gog_cli-0.2.1/src/gog_cli.egg-info/PKG-INFO +193 -0
- gog_cli-0.2.1/src/gog_cli.egg-info/SOURCES.txt +42 -0
- gog_cli-0.2.1/src/gog_cli.egg-info/dependency_links.txt +1 -0
- gog_cli-0.2.1/src/gog_cli.egg-info/entry_points.txt +2 -0
- gog_cli-0.2.1/src/gog_cli.egg-info/requires.txt +6 -0
- gog_cli-0.2.1/src/gog_cli.egg-info/top_level.txt +1 -0
- gog_cli-0.2.1/tests/test_api.py +311 -0
- gog_cli-0.2.1/tests/test_aria2c.py +215 -0
- gog_cli-0.2.1/tests/test_auth.py +342 -0
- gog_cli-0.2.1/tests/test_backup.py +316 -0
- gog_cli-0.2.1/tests/test_cli.py +2303 -0
- gog_cli-0.2.1/tests/test_config.py +160 -0
- gog_cli-0.2.1/tests/test_downloader.py +192 -0
- gog_cli-0.2.1/tests/test_errors.py +82 -0
- gog_cli-0.2.1/tests/test_layout.py +96 -0
- gog_cli-0.2.1/tests/test_output.py +124 -0
- gog_cli-0.2.1/tests/test_prompt.py +78 -0
- gog_cli-0.2.1/tests/test_refresh.py +456 -0
- gog_cli-0.2.1/tests/test_state.py +118 -0
- gog_cli-0.2.1/tests/test_sync.py +248 -0
gog_cli-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gog-cli contributors
|
|
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.
|
gog_cli-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gog-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: CLI tool for backing up DRM-free GOG games.
|
|
5
|
+
Author: gog-cli contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 gog-cli contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/aleksandarristic/gog-cli
|
|
29
|
+
Project-URL: Repository, https://github.com/aleksandarristic/gog-cli
|
|
30
|
+
Project-URL: Bug Tracker, https://github.com/aleksandarristic/gog-cli/issues
|
|
31
|
+
Requires-Python: >=3.12
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: requests>=2.32
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
37
|
+
Requires-Dist: responses>=0.25; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# gog-cli
|
|
42
|
+
|
|
43
|
+
[](https://github.com/aleksandarristic/gog-cli/actions/workflows/ci.yml)
|
|
44
|
+
|
|
45
|
+
`gog` is a Python CLI for backing up a user's owned DRM-free GOG game library.
|
|
46
|
+
|
|
47
|
+
It is focused on safe, scriptable workflows:
|
|
48
|
+
|
|
49
|
+
- list owned games with filtering and fuzzy search
|
|
50
|
+
- plan and execute backups to a local directory
|
|
51
|
+
- preserve metadata needed to audit and restore backups
|
|
52
|
+
- download installers and related files with resumable behavior
|
|
53
|
+
- verify downloaded files when checksums are available
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
Requires Python 3.12 or newer.
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pip install git+https://github.com/aleksandarristic/gog-cli.git
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Development
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
python3 -m venv .venv
|
|
67
|
+
. .venv/bin/activate
|
|
68
|
+
python -m pip install -e ".[dev]"
|
|
69
|
+
python -m pytest
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Run the CLI locally:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
gog --help
|
|
76
|
+
gog list
|
|
77
|
+
gog plan --destination /path/to/backups --all --summary
|
|
78
|
+
gog backup --destination /path/to/backups --games-from games.txt --dry-run
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Basic Workflow
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
gog auth login
|
|
85
|
+
gog refresh
|
|
86
|
+
gog list purchased
|
|
87
|
+
gog plan --destination /path/to/backups --all --storage --check-free-space
|
|
88
|
+
gog backup --destination /path/to/backups --all --yes
|
|
89
|
+
gog list backed-up --destination /path/to/backups
|
|
90
|
+
gog sync --destination /path/to/backups --all --yes
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`gog refresh` updates the local purchased-library and download-metadata caches.
|
|
94
|
+
It does not download game installers. Run it before browsing or filtering newly
|
|
95
|
+
added library metadata.
|
|
96
|
+
|
|
97
|
+
## Browsing Purchased Games
|
|
98
|
+
|
|
99
|
+
`gog list purchased` reads the local cache written by `gog refresh`; it does not
|
|
100
|
+
contact GOG. Human output includes ID, title, release year, genre/category, and
|
|
101
|
+
platforms when those fields are available. JSON output also includes scriptable
|
|
102
|
+
metadata such as `owned`, `release_date`, `genres`, and `is_installable`.
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
gog list purchased
|
|
108
|
+
gog list purchased --format json
|
|
109
|
+
gog list purchased --search witcher
|
|
110
|
+
gog list purchased --search "baldurs gate"
|
|
111
|
+
gog list purchased --platform windows
|
|
112
|
+
gog list purchased --platform linux --search ftl
|
|
113
|
+
gog list purchased --year 1998..2005
|
|
114
|
+
gog list purchased --year 2010..2020 --include-unknown-year
|
|
115
|
+
gog list purchased --genre strategy
|
|
116
|
+
gog list purchased --genre arcade,rts
|
|
117
|
+
gog list purchased --genre strategy --include-unknown-genre
|
|
118
|
+
gog list purchased --search "baldurs gate" --platform linux --format json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Year filters omit games with unknown years by default; use
|
|
122
|
+
`--include-unknown-year` to keep them. Genre filters similarly omit unknown
|
|
123
|
+
genres by default; use `--include-unknown-genre` to keep those rows.
|
|
124
|
+
|
|
125
|
+
## Planning Backups
|
|
126
|
+
|
|
127
|
+
`gog plan` shows the same dry-run plan as `gog backup --dry-run` without
|
|
128
|
+
downloading files or creating backup directories. Use it before long backup runs
|
|
129
|
+
to estimate size, inspect filters, and check destination free space.
|
|
130
|
+
|
|
131
|
+
Examples:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
gog plan --destination /path/to/backups --all
|
|
135
|
+
gog plan --destination /path/to/backups --all --summary
|
|
136
|
+
gog plan --destination /path/to/backups --all --storage
|
|
137
|
+
gog plan --destination /path/to/backups --all --check-free-space
|
|
138
|
+
gog plan --destination /path/to/backups --all --format json
|
|
139
|
+
gog plan --destination /path/to/backups cyberpunk_2077
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Platform and language filters can reduce backup size:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
gog plan --destination /path/to/backups --all --platform linux --storage
|
|
146
|
+
gog plan --destination /path/to/backups --all --platform windows --language en --storage
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Selecting Games
|
|
150
|
+
|
|
151
|
+
Game selectors can be product IDs, slugs, or exact titles. Commands that select
|
|
152
|
+
games accept repeated `--game` flags:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
gog plan --destination /path/to/backups --game witcher_3 --game cyberpunk_2077
|
|
156
|
+
gog backup --destination /path/to/backups --game 123456789 --yes
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For larger curated lists, put selectors in a UTF-8 text file and pass
|
|
160
|
+
`--games-from`. Blank lines and lines whose first non-whitespace character is
|
|
161
|
+
`#` are ignored.
|
|
162
|
+
|
|
163
|
+
Example `games.txt`:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
# first NAS batch
|
|
167
|
+
witcher_3
|
|
168
|
+
cyberpunk_2077
|
|
169
|
+
123456789
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Use the selector file in plan, backup, or sync workflows:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
gog plan --destination /path/to/backups --games-from games.txt --storage
|
|
176
|
+
gog backup --destination /path/to/backups --games-from games.txt --downloader aria2c --yes
|
|
177
|
+
gog sync --destination /path/to/backups --games-from games.txt --dry-run
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`--games-from` is repeatable and combines with repeated `--game` flags. Do not
|
|
181
|
+
combine explicit game selectors with `--all`.
|
|
182
|
+
|
|
183
|
+
## Downloading
|
|
184
|
+
|
|
185
|
+
`gog backup` defaults to the built-in direct downloader. To use `aria2c`, install
|
|
186
|
+
`aria2c` and pass `--downloader aria2c` on an executing backup run:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
gog backup --destination /path/to/backups --games-from games.txt --downloader aria2c --yes
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Without `--yes`, backup and sync commands print a dry-run plan and exit without
|
|
193
|
+
downloading or modifying backup files.
|
gog_cli-0.2.1/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# gog-cli
|
|
2
|
+
|
|
3
|
+
[](https://github.com/aleksandarristic/gog-cli/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
`gog` is a Python CLI for backing up a user's owned DRM-free GOG game library.
|
|
6
|
+
|
|
7
|
+
It is focused on safe, scriptable workflows:
|
|
8
|
+
|
|
9
|
+
- list owned games with filtering and fuzzy search
|
|
10
|
+
- plan and execute backups to a local directory
|
|
11
|
+
- preserve metadata needed to audit and restore backups
|
|
12
|
+
- download installers and related files with resumable behavior
|
|
13
|
+
- verify downloaded files when checksums are available
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Requires Python 3.12 or newer.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install git+https://github.com/aleksandarristic/gog-cli.git
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Development
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
python3 -m venv .venv
|
|
27
|
+
. .venv/bin/activate
|
|
28
|
+
python -m pip install -e ".[dev]"
|
|
29
|
+
python -m pytest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run the CLI locally:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
gog --help
|
|
36
|
+
gog list
|
|
37
|
+
gog plan --destination /path/to/backups --all --summary
|
|
38
|
+
gog backup --destination /path/to/backups --games-from games.txt --dry-run
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Basic Workflow
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
gog auth login
|
|
45
|
+
gog refresh
|
|
46
|
+
gog list purchased
|
|
47
|
+
gog plan --destination /path/to/backups --all --storage --check-free-space
|
|
48
|
+
gog backup --destination /path/to/backups --all --yes
|
|
49
|
+
gog list backed-up --destination /path/to/backups
|
|
50
|
+
gog sync --destination /path/to/backups --all --yes
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`gog refresh` updates the local purchased-library and download-metadata caches.
|
|
54
|
+
It does not download game installers. Run it before browsing or filtering newly
|
|
55
|
+
added library metadata.
|
|
56
|
+
|
|
57
|
+
## Browsing Purchased Games
|
|
58
|
+
|
|
59
|
+
`gog list purchased` reads the local cache written by `gog refresh`; it does not
|
|
60
|
+
contact GOG. Human output includes ID, title, release year, genre/category, and
|
|
61
|
+
platforms when those fields are available. JSON output also includes scriptable
|
|
62
|
+
metadata such as `owned`, `release_date`, `genres`, and `is_installable`.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
gog list purchased
|
|
68
|
+
gog list purchased --format json
|
|
69
|
+
gog list purchased --search witcher
|
|
70
|
+
gog list purchased --search "baldurs gate"
|
|
71
|
+
gog list purchased --platform windows
|
|
72
|
+
gog list purchased --platform linux --search ftl
|
|
73
|
+
gog list purchased --year 1998..2005
|
|
74
|
+
gog list purchased --year 2010..2020 --include-unknown-year
|
|
75
|
+
gog list purchased --genre strategy
|
|
76
|
+
gog list purchased --genre arcade,rts
|
|
77
|
+
gog list purchased --genre strategy --include-unknown-genre
|
|
78
|
+
gog list purchased --search "baldurs gate" --platform linux --format json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Year filters omit games with unknown years by default; use
|
|
82
|
+
`--include-unknown-year` to keep them. Genre filters similarly omit unknown
|
|
83
|
+
genres by default; use `--include-unknown-genre` to keep those rows.
|
|
84
|
+
|
|
85
|
+
## Planning Backups
|
|
86
|
+
|
|
87
|
+
`gog plan` shows the same dry-run plan as `gog backup --dry-run` without
|
|
88
|
+
downloading files or creating backup directories. Use it before long backup runs
|
|
89
|
+
to estimate size, inspect filters, and check destination free space.
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
gog plan --destination /path/to/backups --all
|
|
95
|
+
gog plan --destination /path/to/backups --all --summary
|
|
96
|
+
gog plan --destination /path/to/backups --all --storage
|
|
97
|
+
gog plan --destination /path/to/backups --all --check-free-space
|
|
98
|
+
gog plan --destination /path/to/backups --all --format json
|
|
99
|
+
gog plan --destination /path/to/backups cyberpunk_2077
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Platform and language filters can reduce backup size:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
gog plan --destination /path/to/backups --all --platform linux --storage
|
|
106
|
+
gog plan --destination /path/to/backups --all --platform windows --language en --storage
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Selecting Games
|
|
110
|
+
|
|
111
|
+
Game selectors can be product IDs, slugs, or exact titles. Commands that select
|
|
112
|
+
games accept repeated `--game` flags:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
gog plan --destination /path/to/backups --game witcher_3 --game cyberpunk_2077
|
|
116
|
+
gog backup --destination /path/to/backups --game 123456789 --yes
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For larger curated lists, put selectors in a UTF-8 text file and pass
|
|
120
|
+
`--games-from`. Blank lines and lines whose first non-whitespace character is
|
|
121
|
+
`#` are ignored.
|
|
122
|
+
|
|
123
|
+
Example `games.txt`:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
# first NAS batch
|
|
127
|
+
witcher_3
|
|
128
|
+
cyberpunk_2077
|
|
129
|
+
123456789
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use the selector file in plan, backup, or sync workflows:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
gog plan --destination /path/to/backups --games-from games.txt --storage
|
|
136
|
+
gog backup --destination /path/to/backups --games-from games.txt --downloader aria2c --yes
|
|
137
|
+
gog sync --destination /path/to/backups --games-from games.txt --dry-run
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`--games-from` is repeatable and combines with repeated `--game` flags. Do not
|
|
141
|
+
combine explicit game selectors with `--all`.
|
|
142
|
+
|
|
143
|
+
## Downloading
|
|
144
|
+
|
|
145
|
+
`gog backup` defaults to the built-in direct downloader. To use `aria2c`, install
|
|
146
|
+
`aria2c` and pass `--downloader aria2c` on an executing backup run:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
gog backup --destination /path/to/backups --games-from games.txt --downloader aria2c --yes
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Without `--yes`, backup and sync commands print a dry-run plan and exit without
|
|
153
|
+
downloading or modifying backup files.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gog-cli"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "CLI tool for backing up DRM-free GOG games."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "gog-cli contributors" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"requests>=2.32",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/aleksandarristic/gog-cli"
|
|
22
|
+
Repository = "https://github.com/aleksandarristic/gog-cli"
|
|
23
|
+
"Bug Tracker" = "https://github.com/aleksandarristic/gog-cli/issues"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8",
|
|
28
|
+
"responses>=0.25",
|
|
29
|
+
"ruff>=0.4",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
gog = "gog_cli.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
pythonpath = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "W", "F", "I", "N", "B", "UP", "SIM", "S", "PT"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
"tests/*" = ["S101", "S105", "S107", "S108"]
|
gog_cli-0.2.1/setup.cfg
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""GOG API client and TokenStore protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from gog_cli import log
|
|
11
|
+
from gog_cli.errors import AuthError, NetworkError
|
|
12
|
+
|
|
13
|
+
_log = log.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
_CLIENT_ID = "46899977096215655"
|
|
16
|
+
_CLIENT_SECRET = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9" # noqa: S105 - Public GOG Galaxy OAuth client credential.
|
|
17
|
+
_TOKEN_URL = "https://auth.gog.com/token" # noqa: S105 - URL constant, not a secret.
|
|
18
|
+
|
|
19
|
+
_OWNED_GAMES_URL = "https://embed.gog.com/user/data/games"
|
|
20
|
+
_LIBRARY_URL = "https://embed.gog.com/account/getFilteredProducts"
|
|
21
|
+
_PRODUCT_URL = "https://api.gog.com/products/{product_id}"
|
|
22
|
+
# Unofficial public catalog search endpoint — no authentication required.
|
|
23
|
+
_CATALOG_SEARCH_URL = "https://catalog.gog.com/v1/catalog"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TokenStore(Protocol):
|
|
27
|
+
def load_tokens(self) -> dict:
|
|
28
|
+
"""Return stored tokens: {"access_token": str, "refresh_token": str, "expires_at": str}"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def save_tokens(self, tokens: dict) -> None:
|
|
32
|
+
"""Persist updated tokens after a refresh."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GogApiClient:
|
|
37
|
+
def __init__(self, token_store: TokenStore) -> None:
|
|
38
|
+
self._token_store = token_store
|
|
39
|
+
self._session = requests.Session()
|
|
40
|
+
|
|
41
|
+
def _access_token(self) -> str:
|
|
42
|
+
return self._token_store.load_tokens()["access_token"]
|
|
43
|
+
|
|
44
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
45
|
+
return {"Authorization": f"Bearer {self._access_token()}"}
|
|
46
|
+
|
|
47
|
+
def _refresh_tokens(self) -> None:
|
|
48
|
+
tokens = self._token_store.load_tokens()
|
|
49
|
+
try:
|
|
50
|
+
resp = self._session.get(
|
|
51
|
+
_TOKEN_URL,
|
|
52
|
+
params={
|
|
53
|
+
"client_id": _CLIENT_ID,
|
|
54
|
+
"client_secret": _CLIENT_SECRET,
|
|
55
|
+
"grant_type": "refresh_token",
|
|
56
|
+
"refresh_token": tokens["refresh_token"],
|
|
57
|
+
},
|
|
58
|
+
timeout=30,
|
|
59
|
+
)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
except requests.HTTPError as exc:
|
|
62
|
+
raise AuthError(f"token refresh failed: {exc}") from exc
|
|
63
|
+
except (requests.ConnectionError, requests.Timeout) as exc:
|
|
64
|
+
raise AuthError(f"token refresh network error: {exc}") from exc
|
|
65
|
+
|
|
66
|
+
data = resp.json()
|
|
67
|
+
expires_at = datetime.fromtimestamp(
|
|
68
|
+
datetime.now(tz=UTC).timestamp() + data["expires_in"],
|
|
69
|
+
tz=UTC,
|
|
70
|
+
).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
71
|
+
self._token_store.save_tokens(
|
|
72
|
+
{
|
|
73
|
+
**tokens,
|
|
74
|
+
"access_token": data["access_token"],
|
|
75
|
+
"refresh_token": data["refresh_token"],
|
|
76
|
+
"expires_at": expires_at,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _get(self, url: str, **kwargs: object) -> requests.Response:
|
|
81
|
+
try:
|
|
82
|
+
resp = self._session.get(url, headers=self._auth_headers(), timeout=30, **kwargs)
|
|
83
|
+
except (requests.ConnectionError, requests.Timeout, ConnectionError) as exc:
|
|
84
|
+
raise NetworkError(f"network error: {exc}") from exc
|
|
85
|
+
|
|
86
|
+
if resp.status_code == 401:
|
|
87
|
+
self._refresh_tokens()
|
|
88
|
+
try:
|
|
89
|
+
resp = self._session.get(
|
|
90
|
+
url, headers=self._auth_headers(), timeout=30, **kwargs
|
|
91
|
+
)
|
|
92
|
+
except (requests.ConnectionError, requests.Timeout, ConnectionError) as exc:
|
|
93
|
+
raise NetworkError(f"network error: {exc}") from exc
|
|
94
|
+
if resp.status_code == 401:
|
|
95
|
+
raise AuthError("authentication failed after token refresh")
|
|
96
|
+
|
|
97
|
+
if not resp.ok:
|
|
98
|
+
raise NetworkError(f"HTTP {resp.status_code}: {log.redact(url)}")
|
|
99
|
+
|
|
100
|
+
return resp
|
|
101
|
+
|
|
102
|
+
def get_owned_ids(self) -> list[int]:
|
|
103
|
+
resp = self._get(_OWNED_GAMES_URL)
|
|
104
|
+
return resp.json()["owned"]
|
|
105
|
+
|
|
106
|
+
def get_library_page(self, page: int) -> dict:
|
|
107
|
+
resp = self._get(_LIBRARY_URL, params={"mediaType": 1, "page": page})
|
|
108
|
+
return resp.json()
|
|
109
|
+
|
|
110
|
+
def get_product_downloads(self, product_id: int) -> dict:
|
|
111
|
+
url = _PRODUCT_URL.format(product_id=product_id)
|
|
112
|
+
resp = self._get(url, params={"expand": "downloads"})
|
|
113
|
+
return resp.json()
|
|
114
|
+
|
|
115
|
+
def resolve_downlink_url(self, downlink_url: str) -> tuple[str, str]:
|
|
116
|
+
resp = self._get(downlink_url)
|
|
117
|
+
data = resp.json()
|
|
118
|
+
return data["downlink"], data.get("checksum", "")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def search_catalog(query: str, *, page: int = 1) -> dict:
|
|
122
|
+
"""Search the public GOG catalog without authentication.
|
|
123
|
+
|
|
124
|
+
Uses an unofficial reverse-engineered endpoint; no stability guarantees.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
resp = requests.get(
|
|
128
|
+
_CATALOG_SEARCH_URL,
|
|
129
|
+
params={
|
|
130
|
+
"search": query,
|
|
131
|
+
"limit": 48,
|
|
132
|
+
"page": page,
|
|
133
|
+
"order": "desc:relevance",
|
|
134
|
+
"productType": "in:game",
|
|
135
|
+
},
|
|
136
|
+
timeout=30,
|
|
137
|
+
)
|
|
138
|
+
resp.raise_for_status()
|
|
139
|
+
except requests.HTTPError as exc:
|
|
140
|
+
raise NetworkError(f"catalog search failed: {exc}") from exc
|
|
141
|
+
except (requests.ConnectionError, requests.Timeout, ConnectionError) as exc:
|
|
142
|
+
raise NetworkError(f"catalog search network error: {exc}") from exc
|
|
143
|
+
return resp.json()
|