handle optim errors

This commit is contained in:
rmanach 2025-10-23 17:00:01 +02:00
parent 0ba3e7a9ff
commit f79668195b
3 changed files with 66 additions and 25 deletions

4
.gitignore vendored
View File

@ -8,4 +8,6 @@ data
dist dist
docs docs
*.log *.log
*.err*
*.swp

View File

@ -126,6 +126,7 @@ def main():
logging.info( logging.info(
f"total optimization ({optimized}/{len(result.orig)}): {percent:.2f}% -> {size:.2f} Mb" # noqa f"total optimization ({optimized}/{len(result.orig)}): {percent:.2f}% -> {size:.2f} Mb" # noqa
) )
result.check_errors()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,7 +4,7 @@ import signal
import subprocess import subprocess
import time import time
from concurrent.futures import ProcessPoolExecutor, as_completed from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Optional from typing import Optional
from tqdm import tqdm from tqdm import tqdm
@ -15,7 +15,9 @@ __all__ = ["ImgOptimizer", "OptimizerResult"]
# TODO(rmanach): add argument to set the size or leave it empty for loseless optim # TODO(rmanach): add argument to set the size or leave it empty for loseless optim
def _jpeg_optim(dest_dir: str, file: File) -> tuple["File", Optional["File"]] | None: def _jpeg_optim(
dest_dir: str, file: File
) -> tuple["File", Optional["File"], str | None]:
""" """
Optimize the `file` with `jpegoptim` and put the result in Optimize the `file` with `jpegoptim` and put the result in
`dest_dir` directory keeping file path. `dest_dir` directory keeping file path.
@ -35,22 +37,26 @@ def _jpeg_optim(dest_dir: str, file: File) -> tuple["File", Optional["File"]] |
try: try:
_ = subprocess.run(cmd, shell=True, check=True) _ = subprocess.run(cmd, shell=True, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logging.error("error while running command: %s, err: %s", cmd, e.stderr or "?") logging.debug("error while running command: %s, err: %s", cmd, e.stderr or "?")
return None return file, None, f"command: {cmd} failed, err: {e.stderr}"
except Exception: except Exception:
logging.error("unexpected error while running command: %s", cmd, exc_info=True) logging.debug("unexpected error while running command: %s", cmd, exc_info=True)
return None return (
file,
None,
f"command: {cmd} failed unexpectedly, set debug mode for more details",
)
try: try:
file_optim = File.from_directory(dest_dir, file.name) file_optim = File.from_directory(dest_dir, file.name)
except Exception as e: except Exception as e:
logging.debug("unable to get file: %s after optimization: %s", file, e) logging.debug("unable to get file: %s after optimization: %s", file, e)
return file, None return file, None, None
return file, file_optim return file, file_optim, None
def _optim(dest_dir: str, file: File) -> tuple["File", Optional["File"]] | None: def _optim(dest_dir: str, file: File) -> tuple["File", Optional["File"], str | None]:
""" """
Entry point of `file` optimization selection the handler. Entry point of `file` optimization selection the handler.
NOTE: Must be launched in separated process. NOTE: Must be launched in separated process.
@ -59,7 +65,27 @@ def _optim(dest_dir: str, file: File) -> tuple["File", Optional["File"]] | None:
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
if file.mimetype == FileImgMimetype.JPEG.value: if file.mimetype == FileImgMimetype.JPEG.value:
return _jpeg_optim(dest_dir, file) return _jpeg_optim(dest_dir, file)
return None return file, None, f"mimetype: {file.mimetype} not supported for optimization"
@dataclass(slots=True, frozen=True)
class OptimizerErrors:
_data: dict[str, str] = field(default_factory=dict)
def save(self):
if not len(self._data):
return
with open("optimg.err.csv", "w") as f:
f.write("filepath;err\n")
for filepath, error in self._data.items():
f.write(f"{filepath};{error}\n")
def has_errors(self) -> bool:
return len(self._data) > 0
def add(self, filepath: str, error: str):
self._data[filepath] = error
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
@ -73,6 +99,7 @@ class OptimizerResult:
orig: FileGroup orig: FileGroup
opti: FileGroup opti: FileGroup
optimized: int optimized: int
err: OptimizerErrors
def stats(self) -> tuple[int, float, float]: def stats(self) -> tuple[int, float, float]:
""" """
@ -85,6 +112,13 @@ class OptimizerResult:
size = self.orig._size - self.opti._size size = self.orig._size - self.opti._size
return (self.optimized, percent, size) return (self.optimized, percent, size)
def check_errors(self):
if self.err.has_errors():
logging.error(
"some errors detected during optimization, check 'optimg.err.csv' for details" # noqa
)
self.err.save()
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class ImgOptimizer: class ImgOptimizer:
@ -94,8 +128,8 @@ class ImgOptimizer:
Example: Example:
```python ```python
optimizer = ImgOptimizer("mypath") optimizer = ImgOptimizer()
optimizer.optimize() res = optimizer.optimize(file_group)
``` ```
""" """
@ -134,20 +168,24 @@ class ImgOptimizer:
file_group_optim = FileGroup() file_group_optim = FileGroup()
optimized = 0 optimized = 0
errors = OptimizerErrors()
for f in tqdm( for f in tqdm(
as_completed(futures), total=len(futures), desc="Optimizing..." as_completed(futures), total=len(futures), desc="Optimizing..."
): ):
if (res := f.result()) and res is not None: match f.result():
match res: case (orig, None, err):
case (orig, None): logging.debug(f"optim error for file: {orig}, err: {err}")
logging.debug(f"no optimization for file: {orig}") file_group_optim.add(orig)
file_group_optim.add(orig) errors.add(orig.path, err)
case (orig, opti): case (orig, None, None):
optimized += 1 logging.debug(f"no optimization for file: {orig}")
logging.debug( file_group_optim.add(orig)
f"optimization for file: {orig} -> {(1 - (opti.size / orig.size)) * 100:.2f}%" # noqa case (orig, opti, None):
) optimized += 1
file_group_optim.add(opti) logging.debug(
f"optimization for file: {orig} -> {(1 - (opti.size / orig.size)) * 100:.2f}%" # noqa
)
file_group_optim.add(opti)
logging.info(f"optimization finished in {time.perf_counter() - start:.2f}s") logging.info(f"optimization finished in {time.perf_counter() - start:.2f}s")
return OptimizerResult(file_group, file_group_optim, optimized) return OptimizerResult(file_group, file_group_optim, optimized, errors)