booru-dl 2.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.
Files changed (45) hide show
  1. booru_dl-2.1.0/LICENSE +21 -0
  2. booru_dl-2.1.0/PKG-INFO +216 -0
  3. booru_dl-2.1.0/README.md +181 -0
  4. booru_dl-2.1.0/bdl/__init__.py +27 -0
  5. booru_dl-2.1.0/bdl/__main__.py +8 -0
  6. booru_dl-2.1.0/bdl/blacklist.py +74 -0
  7. booru_dl-2.1.0/bdl/cli.py +849 -0
  8. booru_dl-2.1.0/bdl/config.py +187 -0
  9. booru_dl-2.1.0/bdl/db.py +355 -0
  10. booru_dl-2.1.0/bdl/downloader.py +913 -0
  11. booru_dl-2.1.0/bdl/filters.py +184 -0
  12. booru_dl-2.1.0/bdl/i18n/__init__.py +126 -0
  13. booru_dl-2.1.0/bdl/i18n/en.json +60 -0
  14. booru_dl-2.1.0/bdl/i18n/ru.json +60 -0
  15. booru_dl-2.1.0/bdl/media.py +77 -0
  16. booru_dl-2.1.0/bdl/meta.py +83 -0
  17. booru_dl-2.1.0/bdl/models.py +27 -0
  18. booru_dl-2.1.0/bdl/net/__init__.py +0 -0
  19. booru_dl-2.1.0/bdl/net/doh.py +103 -0
  20. booru_dl-2.1.0/bdl/notifier.py +31 -0
  21. booru_dl-2.1.0/bdl/perceptual.py +50 -0
  22. booru_dl-2.1.0/bdl/preview.py +232 -0
  23. booru_dl-2.1.0/bdl/providers/__init__.py +246 -0
  24. booru_dl-2.1.0/bdl/providers/base.py +232 -0
  25. booru_dl-2.1.0/bdl/providers/client.py +244 -0
  26. booru_dl-2.1.0/bdl/tags/__init__.py +0 -0
  27. booru_dl-2.1.0/bdl/tags/catalog.py +205 -0
  28. booru_dl-2.1.0/bdl/tags/commands.py +83 -0
  29. booru_dl-2.1.0/bdl/tags/families.py +126 -0
  30. booru_dl-2.1.0/bdl/tags/library.py +879 -0
  31. booru_dl-2.1.0/bdl/tags/store.py +309 -0
  32. booru_dl-2.1.0/bdl/tags/suggest.py +202 -0
  33. booru_dl-2.1.0/bdl/tui/__init__.py +12 -0
  34. booru_dl-2.1.0/bdl/tui/style.py +70 -0
  35. booru_dl-2.1.0/bdl/tui/wizard.py +453 -0
  36. booru_dl-2.1.0/booru_dl.egg-info/PKG-INFO +216 -0
  37. booru_dl-2.1.0/booru_dl.egg-info/SOURCES.txt +43 -0
  38. booru_dl-2.1.0/booru_dl.egg-info/dependency_links.txt +1 -0
  39. booru_dl-2.1.0/booru_dl.egg-info/entry_points.txt +2 -0
  40. booru_dl-2.1.0/booru_dl.egg-info/requires.txt +15 -0
  41. booru_dl-2.1.0/booru_dl.egg-info/top_level.txt +1 -0
  42. booru_dl-2.1.0/pyproject.toml +64 -0
  43. booru_dl-2.1.0/setup.cfg +4 -0
  44. booru_dl-2.1.0/tests/test_dl.py +1301 -0
  45. booru_dl-2.1.0/tests/test_tui.py +165 -0
booru_dl-2.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wexqk-boo
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,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: booru-dl
3
+ Version: 2.1.0
4
+ Summary: Async batch downloader and sync tool for booru boards
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/wexqk-boo/Booru-Downloader-py
7
+ Project-URL: Issues, https://github.com/wexqk-boo/Booru-Downloader-py/issues
8
+ Keywords: booru,downloader,gelbooru,rule34,danbooru,asyncio,cli
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Multimedia :: Graphics
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: aiohttp<4,>=3.10
23
+ Requires-Dist: aiofiles>=24.1
24
+ Requires-Dist: aiosqlite>=0.20
25
+ Requires-Dist: imagehash>=4.3
26
+ Requires-Dist: pillow>=10
27
+ Requires-Dist: rapidfuzz>=3
28
+ Requires-Dist: rich>=13
29
+ Requires-Dist: questionary>=2.0
30
+ Requires-Dist: uvloop>=0.21; sys_platform != "win32"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.6; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # bdl
37
+
38
+ Async batch downloader and sync tool for booru boards (Gelbooru, Rule34,
39
+ Xbooru, Safebooru, Danbooru, Konachan, Yande.re, Tbib, e621, e926).
40
+
41
+ ## Features
42
+
43
+ - **Async streaming downloads** — `asyncio` + `aiohttp`, files streamed to disk
44
+ in chunks, concurrency bounded by `asyncio.Semaphore`.
45
+ - **Four-level deduplication** — `uid` in SQLite, the file already present on
46
+ disk, an optional sha256 check, and a perceptual pHash so the same artwork
47
+ reposted to another board under a new id is skipped too. The first three
48
+ levels are free; the pHash has to see the bytes, so a repost is downloaded,
49
+ recognised and then deleted instead of being kept.
50
+ - **Aspect-ratio filtering** — `--wallpapers`, `--avatars`, or a custom
51
+ `--aspect-ratio MIN:MAX`, applied after the API response so nothing extra is
52
+ downloaded. Boards that omit image dimensions are kept by default; pass
53
+ `--aspect-unknown skip` to drop them instead.
54
+ - **Tag-family blacklist** — a root tag pulls in its related sub-tags from
55
+ `~/.config/bdl/blacklist.txt`, plus a per-run `--blacklist` override.
56
+ - **DNS-over-HTTPS and proxy support** — a built-in `DoHResolver` (Cloudflare)
57
+ works around local DNS blocks on image CDNs, and an upstream proxy can be set.
58
+ - **uvloop when available** — the event loop switches to uvloop on every
59
+ platform but Windows, which speeds up a batch of many small transfers.
60
+ - **Typo tolerance** — a tag that is not in the dictionary is matched against
61
+ the popular tags and corrected before the query goes out (`mku` → `miku`).
62
+ - **Image previews** — inside Kitty, finished pictures are drawn once the batch
63
+ ends as a compact gallery of thumbnail rows rather than a column running off
64
+ the screen; `--no-preview` turns them off.
65
+ - **Interactive wizard** — `bdl` on a terminal (or `bdl tui`) walks through the
66
+ download: boards with checkboxes, tags with live tag suggestions, limit, sort
67
+ order, rating, geometry and an optional list of tags to exclude, then prints a
68
+ summary before starting. The interface ships in Russian (default) and English;
69
+ `questionary` supplies the arrow-key menus and `rich` the section headers.
70
+ - **Machine-readable output** — `--json` on `search` and `status`.
71
+
72
+ ## Installation
73
+
74
+ Requires Python 3.11+. The repository is **Booru-Downloader-py**; the console
75
+ command it installs is **`bdl`**.
76
+
77
+ ```bash
78
+ git clone https://github.com/wexqk-boo/Booru-Downloader-py.git
79
+ cd Booru-Downloader-py
80
+ python3 -m venv .venv
81
+ ./.venv/bin/pip install -e . # or: pip install -e '.[dev]' for tests/ruff
82
+ ```
83
+
84
+ The package installs a single entry point: `bdl`.
85
+
86
+ To put it on `PATH`:
87
+
88
+ ```bash
89
+ ./install.sh # symlink the entry point into ~/.local/bin
90
+ ./install.sh --copy # copy it instead
91
+ ./install.sh --remove # uninstall
92
+ ```
93
+
94
+ Run `bdl` on a terminal to open the wizard. Without a terminal it prints usage
95
+ and exits `2`, so scripts never block on a prompt. If an old install left a
96
+ fish function named `bdl`, it shadows the binary — remove it
97
+ (`rm ~/.config/fish/functions/bdl.fish`) and run `functions -e bdl` once;
98
+ `./install.sh --remove` cleans that up together with the stale helper.
99
+
100
+ `rapidfuzz` powers tag suggestions and typo correction; without it the code
101
+ falls back to the standard-library `difflib`. `imagehash` and `pillow` enable
102
+ perceptual dedup, and `uvloop` speeds up the event loop everywhere but Windows.
103
+
104
+ ## Usage
105
+
106
+ ```bash
107
+ # Download every source, tag "miku", wallpaper aspect ratio only
108
+ bdl dl -s all -t "miku" --wallpapers
109
+
110
+ # Preview results without downloading
111
+ bdl search -s gelbooru -t "school_uniform" --limit 20
112
+
113
+ # Download statistics from the SQLite state
114
+ bdl status
115
+
116
+ # Daemon-style sync: repeated passes, already-downloaded posts skipped
117
+ bdl sync -s all -t "rating:general" --interval 900
118
+
119
+ # Blacklist management
120
+ bdl blacklist list
121
+ bdl blacklist add loli
122
+ ```
123
+
124
+ Other subcommands: `tui` (interactive wizard), `prune-stale` (forget DB rows
125
+ whose files are gone), `config` (`get` / `set` / `show`), and `tags` (tag
126
+ suggestions).
127
+
128
+ `prune-stale` only inspects paths under the current output root, so a run
129
+ against a temporary `--out` cannot forget the real library. Use `--all` to
130
+ check every recorded path, or `--out DIR` to pick a different scope.
131
+
132
+ ### Wizard
133
+
134
+ ```bash
135
+ bdl tui # or bare `bdl` on a terminal, which reaches the same wizard
136
+ ```
137
+
138
+ Rating choices are Safe / Suggestive / NSFW / All (`--safe`, `--suggestive`,
139
+ `--nsfw` on the command line), and geometry offers Wallpapers, Avatars, a
140
+ custom `MIN:MAX` range, or no filter. `MIN:MAX` accepts `1.5:1.8`, `1,5:1,8`,
141
+ `1.5/1.8` and width:height shorthands such as `16:9` or `3:4`.
142
+
143
+ The whole wizard is five screens and takes well under a minute: boards, the tag
144
+ query, limit and geometry, an optional line of tags to exclude, then the
145
+ summary. It never asks about concurrency — 8 parallel transfers keep a 100 Mbit
146
+ line busy — and the JSON/sha256/re-download switches stay off unless you pass
147
+ them to `dl` yourself.
148
+
149
+ The wizard asks for the output folder only until one is known: the first answer
150
+ is saved to the config and later runs reuse it silently, unless `BDL_OUT` is
151
+ set, which wins outright and is never written back. The exclusion step is a
152
+ single comma-separated field — a root tag such as `ai` pulls in its whole family,
153
+ an empty answer excludes nothing. Tag fields complete from the local Danbooru
154
+ database as you type, and the run ends with a summary before the final
155
+ confirmation.
156
+
157
+ Common flags for `dl` / `sync`: `--limit`, `--per-page`, `--out`,
158
+ `--concurrency`, `--sidecar`, `--checksum`, `--force`, `--min-score`,
159
+ `--rating-include`, `--max-size-mb`, `--blacklist`, `--no-progress`,
160
+ `--no-preview`, `--no-notify`, `--timeout`.
161
+
162
+ ## Directory layout
163
+
164
+ Files are written as `<out_root>/<source>/<bucket>/[videos/]/<filename>`, with
165
+ all sidecar JSON collected in a `json/` folder:
166
+
167
+ ```
168
+ <out_root>/<source>/
169
+ ├── safe/ # ratings: safe, s, general, g
170
+ │ ├── <file>.jpg
171
+ │ ├── videos/ # mp4, webm, mov, avi, mkv, flv, m4v, wmv
172
+ │ └── json/ # <file>.jpg.json sidecars (with --sidecar)
173
+ ├── suggestive/ # ratings: questionable, q, sensitive
174
+ └── nsfw/ # ratings: explicit, e
175
+ ```
176
+
177
+ The default `out_root` comes from `BDL_OUT`, then the `out` key in the config
178
+ file, then `~/Pictures/booru`. Unknown or missing ratings land in `nsfw/`.
179
+
180
+ ## Configuration
181
+
182
+ - Config file: `~/.config/bdl/config.json`
183
+ - Download state (SQLite): `~/.local/share/bdl/downloads.sqlite3`
184
+ - Tag blacklist: `~/.config/bdl/blacklist.txt`
185
+ - Tag database cache: `~/.cache/bdl/`
186
+ - Log: `~/.local/state/bdl/bdl.log`
187
+
188
+ Environment variables:
189
+
190
+ - `BDL_OUT` — override the output root (`BOORU_DL_OUT` still works).
191
+ - `BDL_LOG_FILE` — duplicate logs to a file (`BOORU_DL_LOG_FILE` still works).
192
+ - `BDL_LANG` — wizard language, `ru` (default) or `en`. Also settable through
193
+ `bdl config set lang en`.
194
+ - `PROXY_URL` — route requests through a proxy.
195
+
196
+ ### Board blocks
197
+
198
+ Xbooru's DAPI needs no API key, but Cloudflare fronts the site and sometimes
199
+ challenges an IP with HTTP 403. Requests to Xbooru therefore carry a full set of
200
+ browser headers. If the IP is still challenged, the affected files are counted
201
+ as `blocked` instead of filling the screen with warnings, and the reason is
202
+ reported once in the batch summary at the end. Setting `PROXY_URL` is the usual
203
+ way out.
204
+ - `bdl config set out ~/Pictures/booru` — default output root for every command
205
+ that takes `--out` (`BDL_OUT` still wins over it).
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ ./.venv/bin/python -m pytest tests -q # unit tests, no network
211
+ ./.venv/bin/ruff check bdl tests
212
+ ```
213
+
214
+ ## License
215
+
216
+ MIT
@@ -0,0 +1,181 @@
1
+ # bdl
2
+
3
+ Async batch downloader and sync tool for booru boards (Gelbooru, Rule34,
4
+ Xbooru, Safebooru, Danbooru, Konachan, Yande.re, Tbib, e621, e926).
5
+
6
+ ## Features
7
+
8
+ - **Async streaming downloads** — `asyncio` + `aiohttp`, files streamed to disk
9
+ in chunks, concurrency bounded by `asyncio.Semaphore`.
10
+ - **Four-level deduplication** — `uid` in SQLite, the file already present on
11
+ disk, an optional sha256 check, and a perceptual pHash so the same artwork
12
+ reposted to another board under a new id is skipped too. The first three
13
+ levels are free; the pHash has to see the bytes, so a repost is downloaded,
14
+ recognised and then deleted instead of being kept.
15
+ - **Aspect-ratio filtering** — `--wallpapers`, `--avatars`, or a custom
16
+ `--aspect-ratio MIN:MAX`, applied after the API response so nothing extra is
17
+ downloaded. Boards that omit image dimensions are kept by default; pass
18
+ `--aspect-unknown skip` to drop them instead.
19
+ - **Tag-family blacklist** — a root tag pulls in its related sub-tags from
20
+ `~/.config/bdl/blacklist.txt`, plus a per-run `--blacklist` override.
21
+ - **DNS-over-HTTPS and proxy support** — a built-in `DoHResolver` (Cloudflare)
22
+ works around local DNS blocks on image CDNs, and an upstream proxy can be set.
23
+ - **uvloop when available** — the event loop switches to uvloop on every
24
+ platform but Windows, which speeds up a batch of many small transfers.
25
+ - **Typo tolerance** — a tag that is not in the dictionary is matched against
26
+ the popular tags and corrected before the query goes out (`mku` → `miku`).
27
+ - **Image previews** — inside Kitty, finished pictures are drawn once the batch
28
+ ends as a compact gallery of thumbnail rows rather than a column running off
29
+ the screen; `--no-preview` turns them off.
30
+ - **Interactive wizard** — `bdl` on a terminal (or `bdl tui`) walks through the
31
+ download: boards with checkboxes, tags with live tag suggestions, limit, sort
32
+ order, rating, geometry and an optional list of tags to exclude, then prints a
33
+ summary before starting. The interface ships in Russian (default) and English;
34
+ `questionary` supplies the arrow-key menus and `rich` the section headers.
35
+ - **Machine-readable output** — `--json` on `search` and `status`.
36
+
37
+ ## Installation
38
+
39
+ Requires Python 3.11+. The repository is **Booru-Downloader-py**; the console
40
+ command it installs is **`bdl`**.
41
+
42
+ ```bash
43
+ git clone https://github.com/wexqk-boo/Booru-Downloader-py.git
44
+ cd Booru-Downloader-py
45
+ python3 -m venv .venv
46
+ ./.venv/bin/pip install -e . # or: pip install -e '.[dev]' for tests/ruff
47
+ ```
48
+
49
+ The package installs a single entry point: `bdl`.
50
+
51
+ To put it on `PATH`:
52
+
53
+ ```bash
54
+ ./install.sh # symlink the entry point into ~/.local/bin
55
+ ./install.sh --copy # copy it instead
56
+ ./install.sh --remove # uninstall
57
+ ```
58
+
59
+ Run `bdl` on a terminal to open the wizard. Without a terminal it prints usage
60
+ and exits `2`, so scripts never block on a prompt. If an old install left a
61
+ fish function named `bdl`, it shadows the binary — remove it
62
+ (`rm ~/.config/fish/functions/bdl.fish`) and run `functions -e bdl` once;
63
+ `./install.sh --remove` cleans that up together with the stale helper.
64
+
65
+ `rapidfuzz` powers tag suggestions and typo correction; without it the code
66
+ falls back to the standard-library `difflib`. `imagehash` and `pillow` enable
67
+ perceptual dedup, and `uvloop` speeds up the event loop everywhere but Windows.
68
+
69
+ ## Usage
70
+
71
+ ```bash
72
+ # Download every source, tag "miku", wallpaper aspect ratio only
73
+ bdl dl -s all -t "miku" --wallpapers
74
+
75
+ # Preview results without downloading
76
+ bdl search -s gelbooru -t "school_uniform" --limit 20
77
+
78
+ # Download statistics from the SQLite state
79
+ bdl status
80
+
81
+ # Daemon-style sync: repeated passes, already-downloaded posts skipped
82
+ bdl sync -s all -t "rating:general" --interval 900
83
+
84
+ # Blacklist management
85
+ bdl blacklist list
86
+ bdl blacklist add loli
87
+ ```
88
+
89
+ Other subcommands: `tui` (interactive wizard), `prune-stale` (forget DB rows
90
+ whose files are gone), `config` (`get` / `set` / `show`), and `tags` (tag
91
+ suggestions).
92
+
93
+ `prune-stale` only inspects paths under the current output root, so a run
94
+ against a temporary `--out` cannot forget the real library. Use `--all` to
95
+ check every recorded path, or `--out DIR` to pick a different scope.
96
+
97
+ ### Wizard
98
+
99
+ ```bash
100
+ bdl tui # or bare `bdl` on a terminal, which reaches the same wizard
101
+ ```
102
+
103
+ Rating choices are Safe / Suggestive / NSFW / All (`--safe`, `--suggestive`,
104
+ `--nsfw` on the command line), and geometry offers Wallpapers, Avatars, a
105
+ custom `MIN:MAX` range, or no filter. `MIN:MAX` accepts `1.5:1.8`, `1,5:1,8`,
106
+ `1.5/1.8` and width:height shorthands such as `16:9` or `3:4`.
107
+
108
+ The whole wizard is five screens and takes well under a minute: boards, the tag
109
+ query, limit and geometry, an optional line of tags to exclude, then the
110
+ summary. It never asks about concurrency — 8 parallel transfers keep a 100 Mbit
111
+ line busy — and the JSON/sha256/re-download switches stay off unless you pass
112
+ them to `dl` yourself.
113
+
114
+ The wizard asks for the output folder only until one is known: the first answer
115
+ is saved to the config and later runs reuse it silently, unless `BDL_OUT` is
116
+ set, which wins outright and is never written back. The exclusion step is a
117
+ single comma-separated field — a root tag such as `ai` pulls in its whole family,
118
+ an empty answer excludes nothing. Tag fields complete from the local Danbooru
119
+ database as you type, and the run ends with a summary before the final
120
+ confirmation.
121
+
122
+ Common flags for `dl` / `sync`: `--limit`, `--per-page`, `--out`,
123
+ `--concurrency`, `--sidecar`, `--checksum`, `--force`, `--min-score`,
124
+ `--rating-include`, `--max-size-mb`, `--blacklist`, `--no-progress`,
125
+ `--no-preview`, `--no-notify`, `--timeout`.
126
+
127
+ ## Directory layout
128
+
129
+ Files are written as `<out_root>/<source>/<bucket>/[videos/]/<filename>`, with
130
+ all sidecar JSON collected in a `json/` folder:
131
+
132
+ ```
133
+ <out_root>/<source>/
134
+ ├── safe/ # ratings: safe, s, general, g
135
+ │ ├── <file>.jpg
136
+ │ ├── videos/ # mp4, webm, mov, avi, mkv, flv, m4v, wmv
137
+ │ └── json/ # <file>.jpg.json sidecars (with --sidecar)
138
+ ├── suggestive/ # ratings: questionable, q, sensitive
139
+ └── nsfw/ # ratings: explicit, e
140
+ ```
141
+
142
+ The default `out_root` comes from `BDL_OUT`, then the `out` key in the config
143
+ file, then `~/Pictures/booru`. Unknown or missing ratings land in `nsfw/`.
144
+
145
+ ## Configuration
146
+
147
+ - Config file: `~/.config/bdl/config.json`
148
+ - Download state (SQLite): `~/.local/share/bdl/downloads.sqlite3`
149
+ - Tag blacklist: `~/.config/bdl/blacklist.txt`
150
+ - Tag database cache: `~/.cache/bdl/`
151
+ - Log: `~/.local/state/bdl/bdl.log`
152
+
153
+ Environment variables:
154
+
155
+ - `BDL_OUT` — override the output root (`BOORU_DL_OUT` still works).
156
+ - `BDL_LOG_FILE` — duplicate logs to a file (`BOORU_DL_LOG_FILE` still works).
157
+ - `BDL_LANG` — wizard language, `ru` (default) or `en`. Also settable through
158
+ `bdl config set lang en`.
159
+ - `PROXY_URL` — route requests through a proxy.
160
+
161
+ ### Board blocks
162
+
163
+ Xbooru's DAPI needs no API key, but Cloudflare fronts the site and sometimes
164
+ challenges an IP with HTTP 403. Requests to Xbooru therefore carry a full set of
165
+ browser headers. If the IP is still challenged, the affected files are counted
166
+ as `blocked` instead of filling the screen with warnings, and the reason is
167
+ reported once in the batch summary at the end. Setting `PROXY_URL` is the usual
168
+ way out.
169
+ - `bdl config set out ~/Pictures/booru` — default output root for every command
170
+ that takes `--out` (`BDL_OUT` still wins over it).
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ ./.venv/bin/python -m pytest tests -q # unit tests, no network
176
+ ./.venv/bin/ruff check bdl tests
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,27 @@
1
+ """Standalone CLI for batch downloading and syncing content from Booru boards.
2
+
3
+ Self-contained: own providers, DoH resolver, models, dedup database and tag
4
+ dictionaries. Runs as ``python -m bdl`` or via the ``bdl`` console script.
5
+ """
6
+
7
+ __version__ = "2.1.0"
8
+
9
+ #: Repository the project is published from.
10
+ REPO_SLUG = "github.com/wexqk-boo/Booru-Downloader-py"
11
+
12
+ #: User-Agent for normal outgoing API and media requests.
13
+ USER_AGENT = f"bdl/{__version__} ({REPO_SLUG})"
14
+
15
+ #: Xbooru has an open DAPI with no keys, but its Cloudflare edge challenges a
16
+ #: client that announces itself as a script, so both the API call and the media
17
+ #: fetch present a full set of browser headers instead.
18
+ XBOORU_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0"
19
+
20
+ XBOORU_HEADERS: dict[str, str] = {
21
+ "User-Agent": XBOORU_USER_AGENT,
22
+ "Referer": "https://xbooru.com/index.php?page=post&s=list",
23
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
24
+ "Accept-Language": "en-US,en;q=0.5",
25
+ }
26
+
27
+ __all__ = ["REPO_SLUG", "USER_AGENT", "XBOORU_HEADERS", "XBOORU_USER_AGENT", "__version__"]
@@ -0,0 +1,8 @@
1
+ """Entry point: python -m bdl ..."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,74 @@
1
+ """Tag blacklist: persistent file and management helpers.
2
+
3
+ Default file: ``~/.config/bdl/blacklist.txt`` (one tag per line, ``#`` starts a
4
+ comment). Posts matching any of these tags are skipped during download, and a
5
+ base tag such as ``ai`` also pulls in its whole family.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from .config import xdg_config_home
13
+ from .tags.families import expand_blacklist_tags
14
+
15
+
16
+ def default_blacklist_path() -> Path:
17
+ return xdg_config_home() / "bdl" / "blacklist.txt"
18
+
19
+
20
+ def load_blacklist(path: Path | str | None = None) -> set[str]:
21
+ """Read the tag list in lowercase; a missing file yields an empty set."""
22
+ file_path = Path(path).expanduser() if path else default_blacklist_path()
23
+ if not file_path.is_file():
24
+ return set()
25
+ tags: set[str] = set()
26
+ for line in file_path.read_text(encoding="utf-8").splitlines():
27
+ tag = line.strip().lower()
28
+ if tag and not tag.startswith("#"):
29
+ tags.add(tag)
30
+ return tags
31
+
32
+
33
+ def save_blacklist(tags: set[str], path: Path | str | None = None) -> Path:
34
+ """Write the sorted tag list and return the file path."""
35
+ file_path = Path(path).expanduser() if path else default_blacklist_path()
36
+ file_path.parent.mkdir(parents=True, exist_ok=True)
37
+ lines = [f"{tag}\n" for tag in sorted(tags)]
38
+ file_path.write_text("".join(lines), encoding="utf-8")
39
+ return file_path
40
+
41
+
42
+ def expand_tag(tag: str) -> set[str]:
43
+ """Tag family: the tag itself plus related sub-tags (ai/furry/guro/...)."""
44
+ clean = tag.strip().lower()
45
+ if not clean:
46
+ return set()
47
+ expanded = set(expand_blacklist_tags(clean))
48
+ expanded.add(clean)
49
+ return expanded
50
+
51
+
52
+ def add_tag(tag: str, path: Path | str | None = None) -> int:
53
+ """Add a tag and its family; return how many entries were added."""
54
+ expanded = expand_tag(tag)
55
+ if not expanded:
56
+ return 0
57
+ tags = load_blacklist(path)
58
+ before = len(tags)
59
+ tags |= expanded
60
+ if len(tags) != before:
61
+ save_blacklist(tags, path)
62
+ return len(tags) - before
63
+
64
+
65
+ def remove_tag(tag: str, path: Path | str | None = None) -> int:
66
+ """Remove a tag and its family if present; return how many entries were removed."""
67
+ clean = tag.strip().lower()
68
+ tags = load_blacklist(path)
69
+ to_remove = expand_tag(clean) & tags
70
+ if not to_remove:
71
+ return 0
72
+ tags -= to_remove
73
+ save_blacklist(tags, path)
74
+ return len(to_remove)