/
/
/
1"""
2Fail when a provider or controller icon exceeds its size budget.
3
4Icons (``icon.svg``, ``icon_dark.svg``, ``icon.png``, ...) ship inside the package and are served
5to every UI client, so an unoptimized export bloats the install and the wire. Budgets:
6
7- SVG: 5 KB (vector, should stay tiny).
8- PNG: 20 KB (raster, needs more headroom than vector).
9
10New icons must stay within budget; icons that already exceeded it when this check was introduced are
11grandfathered through ``scripts/lint_baselines/oversized_provider_icons.txt`` and should be shrunk
12over time (optimize svgs with a tool such as ``svgo``, and re-export/compress oversized pngs).
13
14Usage:
15 uv run -m scripts.check_provider_icons
16 uv run -m scripts.check_provider_icons --update-baseline # after (re)optimizing icons
17"""
18
19from __future__ import annotations
20
21import sys
22from pathlib import Path
23
24from scripts.lint_baseline import diff_baseline, load_baseline, render_baseline
25
26# ruff: noqa: T201
27
28# repo paths (this file lives at <repo>/scripts/check_provider_icons.py)
29REPO_ROOT = Path(__file__).resolve().parents[1]
30BASELINE_PATH = REPO_ROOT / "scripts" / "lint_baselines" / "oversized_provider_icons.txt"
31
32# Icons live one level below these roots (``<root>/<name>/icon*.svg|png``).
33ICON_ROOTS = (
34 REPO_ROOT / "music_assistant" / "providers",
35 REPO_ROOT / "music_assistant" / "controllers",
36)
37
38# Maximum allowed size per format. Raster pngs get more headroom than vector svgs.
39MAX_SIZE_BY_SUFFIX = {".svg": 5 * 1024, ".png": 20 * 1024}
40
41# The exact icon filenames that get served to clients; keep in sync with detect_provider_icons
42# in music_assistant/helpers/images.py. Only these are size-checked, so unrelated svg/png assets
43# that may live in a provider/controller folder are left alone.
44ICON_FILENAMES = tuple(
45 f"{stem}{suffix}"
46 for stem in ("icon", "icon_dark", "icon_monochrome")
47 for suffix in MAX_SIZE_BY_SUFFIX
48)
49
50_BASELINE_HEADER = (
51 "Provider/controller icons that already exceeded their size budget when the check was "
52 "introduced.\nNew icons must stay within budget (svg <= 5 KB, png <= 20 KB); shrink these and "
53 "drop them from this list.\nRegenerate with: uv run -m scripts.check_provider_icons "
54 "--update-baseline"
55)
56
57
58def find_oversized_icons() -> dict[str, int]:
59 """Return ``{repo-relative path: 1}`` for every icon file larger than its per-format budget."""
60 # Match only the known icon filenames one level below each root (``<root>/<name>/<icon>``),
61 # so unrelated svg/png assets in a provider/controller folder are never flagged.
62 oversized: dict[str, int] = {}
63 for root in ICON_ROOTS:
64 for name in ICON_FILENAMES:
65 for icon_path in sorted(root.glob(f"*/{name}")):
66 if icon_path.stat().st_size > MAX_SIZE_BY_SUFFIX[icon_path.suffix.lower()]:
67 oversized[icon_path.relative_to(REPO_ROOT).as_posix()] = 1
68 return oversized
69
70
71def main(argv: list[str] | None = None) -> int:
72 """Report icons that newly exceed their size budget; return 1 when any were found."""
73 argv = sys.argv[1:] if argv is None else argv
74 current = find_oversized_icons()
75 if "--update-baseline" in argv:
76 BASELINE_PATH.write_text(render_baseline(current, _BASELINE_HEADER), encoding="utf-8")
77 print(f"Wrote {len(current)} entries to {BASELINE_PATH.relative_to(REPO_ROOT)}")
78 return 0
79 regressions, improvements = diff_baseline(current, load_baseline(BASELINE_PATH))
80 if not regressions and not improvements:
81 return 0
82 if regressions:
83 print("Icon(s) exceed their size budget (svg <= 5 KB, png <= 20 KB):", file=sys.stderr)
84 for path in sorted(regressions):
85 budget = MAX_SIZE_BY_SUFFIX[Path(path).suffix.lower()]
86 size = (REPO_ROOT / path).stat().st_size
87 print(f" {path} ({size} bytes, budget {budget // 1024} KB)", file=sys.stderr)
88 print("Optimize them to fit the budget.", file=sys.stderr)
89 if improvements:
90 print(
91 "These icons are now within budget; drop them from the baseline with "
92 "`uv run -m scripts.check_provider_icons --update-baseline`:",
93 file=sys.stderr,
94 )
95 for path in sorted(improvements):
96 print(f" {path}", file=sys.stderr)
97 return 1
98
99
100if __name__ == "__main__":
101 raise SystemExit(main())
102