webpthumb 0.1.0__py3-none-any.whl
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.
- webpthumb/__init__.py +15 -0
- webpthumb/__main__.py +4 -0
- webpthumb/api.py +125 -0
- webpthumb/cli.py +321 -0
- webpthumb/core.py +245 -0
- webpthumb/daemon.py +368 -0
- webpthumb/options.py +34 -0
- webpthumb/watch.py +229 -0
- webpthumb-0.1.0.dist-info/METADATA +142 -0
- webpthumb-0.1.0.dist-info/RECORD +13 -0
- webpthumb-0.1.0.dist-info/WHEEL +5 -0
- webpthumb-0.1.0.dist-info/entry_points.txt +2 -0
- webpthumb-0.1.0.dist-info/top_level.txt +1 -0
webpthumb/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""WebP thumbnails for images and PDFs."""
|
|
2
|
+
|
|
3
|
+
from .core import ThumbError, ThumbResult, make_thumbnail, write_thumbnail
|
|
4
|
+
from .options import ThumbOptions
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ThumbError",
|
|
10
|
+
"ThumbOptions",
|
|
11
|
+
"ThumbResult",
|
|
12
|
+
"make_thumbnail",
|
|
13
|
+
"write_thumbnail",
|
|
14
|
+
"__version__",
|
|
15
|
+
]
|
webpthumb/__main__.py
ADDED
webpthumb/api.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""HTTP endpoint that returns a WebP thumbnail for an uploaded image or PDF."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, HTTPException, UploadFile
|
|
10
|
+
from fastapi.responses import Response
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .core import ThumbError, make_thumbnail
|
|
14
|
+
from .options import ThumbOptions
|
|
15
|
+
|
|
16
|
+
_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _env_int(name: str, default: int) -> int:
|
|
20
|
+
raw = os.environ.get(name)
|
|
21
|
+
if not raw:
|
|
22
|
+
return default
|
|
23
|
+
try:
|
|
24
|
+
return int(raw)
|
|
25
|
+
except ValueError:
|
|
26
|
+
return default
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _env_optional_int(name: str) -> int | None:
|
|
30
|
+
raw = os.environ.get(name)
|
|
31
|
+
if not raw:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return int(raw)
|
|
35
|
+
except ValueError:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _env_optional_float(name: str) -> float | None:
|
|
40
|
+
raw = os.environ.get(name)
|
|
41
|
+
if not raw:
|
|
42
|
+
return None
|
|
43
|
+
try:
|
|
44
|
+
return float(raw)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def upload_limit_bytes() -> int:
|
|
50
|
+
megabytes = _env_int("WEBTHUMB_MAX_UPLOAD_MB", 32)
|
|
51
|
+
if megabytes < 1:
|
|
52
|
+
megabytes = 32
|
|
53
|
+
return megabytes * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def safe_stem(filename: str | None) -> str:
|
|
57
|
+
stem = Path(filename or "thumbnail").stem
|
|
58
|
+
cleaned = _SAFE.sub("_", stem).strip("._")
|
|
59
|
+
return (cleaned or "thumbnail")[:120]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_app(max_upload_bytes: int | None = None) -> FastAPI:
|
|
63
|
+
"""Build the API. Query defaults fall back to ``WEBTHUMB_*`` environment variables."""
|
|
64
|
+
limit = upload_limit_bytes() if max_upload_bytes is None else max_upload_bytes
|
|
65
|
+
app = FastAPI(title="webpthumb", version=__version__)
|
|
66
|
+
app.state.max_upload_bytes = limit
|
|
67
|
+
|
|
68
|
+
@app.get("/health")
|
|
69
|
+
def health() -> dict[str, str]:
|
|
70
|
+
return {"status": "ok"}
|
|
71
|
+
|
|
72
|
+
@app.post("/v1/thumbnail")
|
|
73
|
+
async def thumbnail(
|
|
74
|
+
file: UploadFile,
|
|
75
|
+
width: int | None = None,
|
|
76
|
+
height: int | None = None,
|
|
77
|
+
quality: int | None = None,
|
|
78
|
+
page: int = 1,
|
|
79
|
+
crop_top: float | None = None,
|
|
80
|
+
) -> Response:
|
|
81
|
+
data = await _read_limited(file, app.state.max_upload_bytes)
|
|
82
|
+
if not data:
|
|
83
|
+
raise HTTPException(status_code=400, detail="empty upload")
|
|
84
|
+
resolved_width = width if width is not None else _env_int("WEBTHUMB_WIDTH", 256)
|
|
85
|
+
resolved_height = height if height is not None else _env_optional_int("WEBTHUMB_HEIGHT")
|
|
86
|
+
resolved_quality = quality if quality is not None else _env_int("WEBTHUMB_QUALITY", 80)
|
|
87
|
+
resolved_crop = crop_top if crop_top is not None else _env_optional_float("WEBTHUMB_CROP_TOP")
|
|
88
|
+
filename = file.filename or "thumbnail"
|
|
89
|
+
try:
|
|
90
|
+
options = ThumbOptions(
|
|
91
|
+
width=resolved_width,
|
|
92
|
+
height=resolved_height,
|
|
93
|
+
quality=resolved_quality,
|
|
94
|
+
crop_top=resolved_crop,
|
|
95
|
+
)
|
|
96
|
+
webp = make_thumbnail(data, filename, options, page=page)
|
|
97
|
+
except ThumbError as exc:
|
|
98
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
99
|
+
except ValueError as exc:
|
|
100
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
101
|
+
stem = safe_stem(filename)
|
|
102
|
+
return Response(
|
|
103
|
+
content=webp,
|
|
104
|
+
media_type="image/webp",
|
|
105
|
+
headers={"Content-Disposition": f'inline; filename="{stem}.webp"'},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return app
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _read_limited(file: UploadFile, limit: int) -> bytes:
|
|
112
|
+
chunks: list[bytes] = []
|
|
113
|
+
total = 0
|
|
114
|
+
while True:
|
|
115
|
+
block = await file.read(1024 * 1024)
|
|
116
|
+
if not block:
|
|
117
|
+
break
|
|
118
|
+
total += len(block)
|
|
119
|
+
if total > limit:
|
|
120
|
+
raise HTTPException(status_code=413, detail="upload exceeds limit")
|
|
121
|
+
chunks.append(block)
|
|
122
|
+
return b"".join(chunks)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
app = create_app()
|
webpthumb/cli.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Command line: convert files, serve HTTP, or watch a directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from pdfthumb import generate_thumbnails
|
|
14
|
+
|
|
15
|
+
from .api import create_app
|
|
16
|
+
from .core import ThumbResult, is_image_name, render_file
|
|
17
|
+
from .daemon import start_watcher, stop_watcher, watcher_status
|
|
18
|
+
from .options import ThumbOptions
|
|
19
|
+
from .watch import run_watch
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("webpthumb")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _env_flag(name: str) -> bool:
|
|
25
|
+
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _build_options(args: argparse.Namespace) -> ThumbOptions:
|
|
29
|
+
if args.width is not None:
|
|
30
|
+
width = args.width
|
|
31
|
+
else:
|
|
32
|
+
width = int(os.environ.get("WEBTHUMB_WIDTH", "256"))
|
|
33
|
+
if args.height is not None:
|
|
34
|
+
height = args.height
|
|
35
|
+
else:
|
|
36
|
+
raw_height = os.environ.get("WEBTHUMB_HEIGHT")
|
|
37
|
+
height = int(raw_height) if raw_height else None
|
|
38
|
+
if args.quality is not None:
|
|
39
|
+
quality = args.quality
|
|
40
|
+
else:
|
|
41
|
+
quality = int(os.environ.get("WEBTHUMB_QUALITY", "80"))
|
|
42
|
+
pages = args.pages if args.pages is not None else os.environ.get("WEBTHUMB_PAGES", "1")
|
|
43
|
+
if args.crop_top is not None:
|
|
44
|
+
crop_top = args.crop_top
|
|
45
|
+
else:
|
|
46
|
+
raw_crop = os.environ.get("WEBTHUMB_CROP_TOP")
|
|
47
|
+
crop_top = float(raw_crop) if raw_crop else None
|
|
48
|
+
return ThumbOptions(
|
|
49
|
+
width=width,
|
|
50
|
+
height=height,
|
|
51
|
+
quality=quality,
|
|
52
|
+
pages=pages,
|
|
53
|
+
crop_top=crop_top,
|
|
54
|
+
overwrite=bool(args.overwrite) or _env_flag("WEBTHUMB_OVERWRITE"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _add_common(parser: argparse.ArgumentParser) -> None:
|
|
59
|
+
parser.add_argument("-w", "--width", type=int, default=None, help="thumbnail width (default 256)")
|
|
60
|
+
parser.add_argument("--height", type=int, default=None, help="maximum height; fit inside width x height")
|
|
61
|
+
parser.add_argument("-q", "--quality", type=int, default=None, help="WebP quality 0-100 (default 80)")
|
|
62
|
+
parser.add_argument("-p", "--pages", default=None, help="PDF pages, e.g. 1, 1,3-5, all (default 1)")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"-c",
|
|
65
|
+
"--crop-top",
|
|
66
|
+
nargs="?",
|
|
67
|
+
const=1.0,
|
|
68
|
+
type=float,
|
|
69
|
+
default=None,
|
|
70
|
+
help="PDF only: keep the top band, height = width x RATIO (default 1 when the flag has no value)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument("--overwrite", action="store_true", help="replace an existing thumbnail")
|
|
73
|
+
parser.add_argument("-r", "--recursive", action="store_true", help="walk directories recursively")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
prog="webpthumb",
|
|
79
|
+
description="Write a WebP thumbnail for an image or a PDF.",
|
|
80
|
+
)
|
|
81
|
+
commands = parser.add_subparsers(dest="cmd", required=True)
|
|
82
|
+
|
|
83
|
+
convert = commands.add_parser("convert", help="thumbnail files or directories")
|
|
84
|
+
convert.add_argument("inputs", nargs="+", help="image files, PDF files, or directories")
|
|
85
|
+
convert.add_argument("-o", "--output", default=None, help="output directory (default ./thumbnails)")
|
|
86
|
+
convert.add_argument("-j", "--jobs", type=int, default=None, help="parallel workers (default: CPU count)")
|
|
87
|
+
_add_common(convert)
|
|
88
|
+
|
|
89
|
+
serve = commands.add_parser("serve", help="serve POST /v1/thumbnail")
|
|
90
|
+
serve.add_argument("--host", default=None, help="bind address (default 127.0.0.1, or WEBTHUMB_HOST)")
|
|
91
|
+
serve.add_argument("--port", type=int, default=None, help="bind port (default 8080, or WEBTHUMB_PORT)")
|
|
92
|
+
|
|
93
|
+
watch = commands.add_parser("watch", help="keep thumbnailing files added to a directory")
|
|
94
|
+
_add_watch_args(watch, background_flag=True)
|
|
95
|
+
|
|
96
|
+
start = commands.add_parser("start", help="start the directory watcher in the background")
|
|
97
|
+
_add_watch_args(start, background_flag=False)
|
|
98
|
+
|
|
99
|
+
commands.add_parser("stop", help="stop the background watcher")
|
|
100
|
+
commands.add_parser("status", help="show whether the background watcher is running")
|
|
101
|
+
return parser
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _add_watch_args(parser: argparse.ArgumentParser, *, background_flag: bool) -> None:
|
|
105
|
+
parser.add_argument("input", help="directory to watch")
|
|
106
|
+
parser.add_argument("-o", "--output", default=None, help="output directory (or WEBTHUMB_OUTPUT)")
|
|
107
|
+
_add_common(parser)
|
|
108
|
+
if background_flag:
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"-b",
|
|
111
|
+
"--background",
|
|
112
|
+
action="store_true",
|
|
113
|
+
help="run in the background and return",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _iter_files(root: Path, recursive: bool) -> list[Path]:
|
|
118
|
+
if recursive:
|
|
119
|
+
files = [path for path in root.rglob("*") if path.is_file() and not path.name.startswith(".")]
|
|
120
|
+
else:
|
|
121
|
+
files = [path for path in root.iterdir() if path.is_file() and not path.name.startswith(".")]
|
|
122
|
+
return sorted(files)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _collect(inputs: list[str], recursive: bool) -> tuple[list[Path], list[tuple[Path, Path]], list[str]]:
|
|
126
|
+
"""Split inputs into PDF paths (for pdfthumb), image jobs, and error strings."""
|
|
127
|
+
pdf_inputs: list[Path] = []
|
|
128
|
+
images: list[tuple[Path, Path]] = []
|
|
129
|
+
errors: list[str] = []
|
|
130
|
+
seen: set[Path] = set()
|
|
131
|
+
for raw in inputs:
|
|
132
|
+
path = Path(raw).expanduser()
|
|
133
|
+
if path.is_dir():
|
|
134
|
+
pdf_inputs.append(path)
|
|
135
|
+
for found in _iter_files(path, recursive):
|
|
136
|
+
if found.suffix.lower() == ".pdf" or not is_image_name(found.name):
|
|
137
|
+
continue
|
|
138
|
+
resolved = found.resolve()
|
|
139
|
+
if resolved in seen:
|
|
140
|
+
continue
|
|
141
|
+
seen.add(resolved)
|
|
142
|
+
images.append((found, found.parent.relative_to(path)))
|
|
143
|
+
elif path.is_file():
|
|
144
|
+
if path.suffix.lower() == ".pdf":
|
|
145
|
+
pdf_inputs.append(path)
|
|
146
|
+
elif is_image_name(path.name):
|
|
147
|
+
resolved = path.resolve()
|
|
148
|
+
if resolved not in seen:
|
|
149
|
+
seen.add(resolved)
|
|
150
|
+
images.append((path, Path(".")))
|
|
151
|
+
else:
|
|
152
|
+
errors.append(f"unsupported format: {path}")
|
|
153
|
+
else:
|
|
154
|
+
errors.append(f"not found: {path}")
|
|
155
|
+
return pdf_inputs, images, errors
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _print_result(result: ThumbResult) -> None:
|
|
159
|
+
if result.status == "error":
|
|
160
|
+
print(f"failed {result.source}: {result.error}", file=sys.stderr)
|
|
161
|
+
elif result.status == "skipped":
|
|
162
|
+
print(f"skipped {result.source} -> {result.output}")
|
|
163
|
+
else:
|
|
164
|
+
print(f"ok {result.source} -> {result.output}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_convert(args: argparse.Namespace) -> int:
|
|
168
|
+
try:
|
|
169
|
+
options = _build_options(args)
|
|
170
|
+
options.validate()
|
|
171
|
+
except ValueError as exc:
|
|
172
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
173
|
+
return 2
|
|
174
|
+
if args.jobs is not None:
|
|
175
|
+
jobs = args.jobs
|
|
176
|
+
else:
|
|
177
|
+
raw_jobs = os.environ.get("WEBTHUMB_JOBS")
|
|
178
|
+
jobs = int(raw_jobs) if raw_jobs else (os.cpu_count() or 1)
|
|
179
|
+
if jobs < 1:
|
|
180
|
+
print("error: jobs must be positive", file=sys.stderr)
|
|
181
|
+
return 2
|
|
182
|
+
output = Path(args.output or os.environ.get("WEBTHUMB_OUTPUT", "thumbnails"))
|
|
183
|
+
recursive = bool(args.recursive) or _env_flag("WEBTHUMB_RECURSIVE")
|
|
184
|
+
pdf_inputs, images, errors = _collect(args.inputs, recursive)
|
|
185
|
+
failed = len(errors)
|
|
186
|
+
for message in errors:
|
|
187
|
+
print(f"failed {message}", file=sys.stderr)
|
|
188
|
+
|
|
189
|
+
if pdf_inputs:
|
|
190
|
+
try:
|
|
191
|
+
pdf_results = generate_thumbnails(
|
|
192
|
+
pdf_inputs,
|
|
193
|
+
output,
|
|
194
|
+
width=options.width,
|
|
195
|
+
height=options.height,
|
|
196
|
+
quality=options.quality,
|
|
197
|
+
pages=options.pages,
|
|
198
|
+
crop_top=options.crop_top,
|
|
199
|
+
recursive=recursive,
|
|
200
|
+
overwrite=options.overwrite,
|
|
201
|
+
jobs=jobs,
|
|
202
|
+
)
|
|
203
|
+
except ValueError as exc:
|
|
204
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
205
|
+
return 2
|
|
206
|
+
for result in pdf_results:
|
|
207
|
+
if result.status == "error":
|
|
208
|
+
print(f"failed {result.input}: {result.error}", file=sys.stderr)
|
|
209
|
+
failed += 1
|
|
210
|
+
elif result.status == "skipped":
|
|
211
|
+
print(f"skipped {result.input} -> {result.output}")
|
|
212
|
+
else:
|
|
213
|
+
print(f"ok {result.input} -> {result.output}")
|
|
214
|
+
|
|
215
|
+
def _render(item: tuple[Path, Path]) -> list[ThumbResult]:
|
|
216
|
+
src, relative = item
|
|
217
|
+
return render_file(src, output, options, relative_dir=relative)
|
|
218
|
+
|
|
219
|
+
if jobs == 1 or len(images) < 2:
|
|
220
|
+
rendered = [_render(item) for item in images]
|
|
221
|
+
else:
|
|
222
|
+
with ThreadPoolExecutor(max_workers=min(jobs, len(images))) as pool:
|
|
223
|
+
rendered = list(pool.map(_render, images))
|
|
224
|
+
for results in rendered:
|
|
225
|
+
for result in results:
|
|
226
|
+
_print_result(result)
|
|
227
|
+
if result.status == "error":
|
|
228
|
+
failed += 1
|
|
229
|
+
return 1 if failed else 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _start_embedded_watch() -> None:
|
|
233
|
+
watch = os.environ.get("WEBTHUMB_WATCH")
|
|
234
|
+
output = os.environ.get("WEBTHUMB_OUTPUT")
|
|
235
|
+
if not watch and not output:
|
|
236
|
+
return
|
|
237
|
+
if not watch or not output:
|
|
238
|
+
log.error("set both WEBTHUMB_WATCH and WEBTHUMB_OUTPUT to watch while serving")
|
|
239
|
+
return
|
|
240
|
+
try:
|
|
241
|
+
options = ThumbOptions(
|
|
242
|
+
width=int(os.environ.get("WEBTHUMB_WIDTH", "256")),
|
|
243
|
+
height=int(os.environ["WEBTHUMB_HEIGHT"]) if os.environ.get("WEBTHUMB_HEIGHT") else None,
|
|
244
|
+
quality=int(os.environ.get("WEBTHUMB_QUALITY", "80")),
|
|
245
|
+
pages=os.environ.get("WEBTHUMB_PAGES", "1"),
|
|
246
|
+
crop_top=float(os.environ["WEBTHUMB_CROP_TOP"]) if os.environ.get("WEBTHUMB_CROP_TOP") else None,
|
|
247
|
+
overwrite=_env_flag("WEBTHUMB_OVERWRITE"),
|
|
248
|
+
)
|
|
249
|
+
options.validate()
|
|
250
|
+
except ValueError as exc:
|
|
251
|
+
log.error("watcher options: %s", exc)
|
|
252
|
+
return
|
|
253
|
+
thread = threading.Thread(
|
|
254
|
+
target=run_watch,
|
|
255
|
+
args=(watch, output, options),
|
|
256
|
+
kwargs={
|
|
257
|
+
"recursive": _env_flag("WEBTHUMB_RECURSIVE"),
|
|
258
|
+
"install_signals": False,
|
|
259
|
+
},
|
|
260
|
+
name="webpthumb-watch",
|
|
261
|
+
daemon=True,
|
|
262
|
+
)
|
|
263
|
+
thread.start()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
267
|
+
import uvicorn
|
|
268
|
+
|
|
269
|
+
host = args.host or os.environ.get("WEBTHUMB_HOST", "127.0.0.1")
|
|
270
|
+
port = args.port if args.port is not None else int(os.environ.get("WEBTHUMB_PORT", "8080"))
|
|
271
|
+
if not 1 <= port <= 65535:
|
|
272
|
+
print("error: port must be from 1 to 65535", file=sys.stderr)
|
|
273
|
+
return 2
|
|
274
|
+
_start_embedded_watch()
|
|
275
|
+
uvicorn.run(create_app(), host=host, port=port)
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _run_watch_command(args: argparse.Namespace, *, background: bool) -> int:
|
|
280
|
+
try:
|
|
281
|
+
options = _build_options(args)
|
|
282
|
+
options.validate()
|
|
283
|
+
except ValueError as exc:
|
|
284
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
285
|
+
return 2
|
|
286
|
+
output = args.output or os.environ.get("WEBTHUMB_OUTPUT")
|
|
287
|
+
if not output:
|
|
288
|
+
print("error: watch needs -o or WEBTHUMB_OUTPUT", file=sys.stderr)
|
|
289
|
+
return 2
|
|
290
|
+
recursive = bool(args.recursive) or _env_flag("WEBTHUMB_RECURSIVE")
|
|
291
|
+
if background:
|
|
292
|
+
return start_watcher(args.input, output, options, recursive=recursive)
|
|
293
|
+
return run_watch(args.input, output, options, recursive=recursive)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def cmd_watch(args: argparse.Namespace) -> int:
|
|
297
|
+
return _run_watch_command(args, background=bool(args.background))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def cmd_start(args: argparse.Namespace) -> int:
|
|
301
|
+
return _run_watch_command(args, background=True)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def main(argv: list[str] | None = None) -> int:
|
|
305
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
306
|
+
parser = build_parser()
|
|
307
|
+
args = parser.parse_args(argv)
|
|
308
|
+
if args.cmd == "convert":
|
|
309
|
+
return cmd_convert(args)
|
|
310
|
+
if args.cmd == "serve":
|
|
311
|
+
return cmd_serve(args)
|
|
312
|
+
if args.cmd == "watch":
|
|
313
|
+
return cmd_watch(args)
|
|
314
|
+
if args.cmd == "start":
|
|
315
|
+
return cmd_start(args)
|
|
316
|
+
if args.cmd == "stop":
|
|
317
|
+
return stop_watcher()
|
|
318
|
+
if args.cmd == "status":
|
|
319
|
+
return watcher_status()
|
|
320
|
+
parser.error(f"unknown command {args.cmd}")
|
|
321
|
+
return 2
|