Mini EQ lets you adjust the sound from your desktop before it reaches your speakers or headphones.
Use ten compact bands to shape bass, mids, and treble, save presets for different outputs, compare the processed sound with the original, and monitor levels while you listen.
-Import Equalizer APO and AutoEq presets when you want to start from an existing correction for your headphones or speakers.
+Search AutoEq headphone corrections from the app, preview the curve, or import Equalizer APO presets from local files.
Older release.
Changed.
") + monkeypatch.setattr(window_layout.Adw, "AboutDialog", FakeAboutDialog) + monkeypatch.setattr(window_layout, "about_release_notes", lambda version: notes) + + window_layout.MiniEqWindowLayoutMixin.show_about_dialog(fake_window) + + dialog = FakeAboutDialog.instances[0] + assert dialog.properties["release_notes"] == "Changed.
" + assert dialog.properties["release_notes_version"] == "1.2.3" + assert dialog.properties["version"] == window_layout.__version__ + assert dialog.presented_to is fake_window + + +def test_visual_layout_height_uses_startup_height_before_first_allocation() -> None: + fake_window = SimpleNamespace( + initial_layout_height=720, + default_min_window_height=600, + compact_min_window_height=600, + get_allocated_height=lambda: 0, + ) + + assert window_layout.visual_layout_height(fake_window, None) == 720 + + +def test_visual_layout_height_prefers_real_allocation() -> None: + fake_window = SimpleNamespace( + initial_layout_height=720, + default_min_window_height=600, + compact_min_window_height=600, + get_allocated_height=lambda: 840, + ) + + assert window_layout.visual_layout_height(fake_window, None) == 840 + assert window_layout.visual_layout_height(fake_window, 960) == 960 + + def test_on_close_request_starts_custom_shutdown_sequence() -> None: calls: list[str] = [] fake_window = SimpleNamespace( @@ -519,12 +584,14 @@ def test_import_apo_updates_provisional_curve_status_and_control_state(tmp_path) set_status=lambda message: statuses.append(message), notify_control_state_changed=lambda: calls.append("notify"), ) + fake_window.import_apo_preset_path = MethodType(window.MiniEqWindow.import_apo_preset_path, fake_window) window.MiniEqWindow.on_import_apo_done(fake_window, FakeOpenDialog(str(apo_path)), None) assert fake_window.selected_band_index is None assert fake_window.current_preset_name is None assert fake_window.saved_preset_signature == "imported-signature" + assert fake_window.output_preset_auto_applied is False assert fake_window.output_preset_curve_auto_loaded is False assert statuses == ["Imported APO curve"] assert calls == [ @@ -537,6 +604,51 @@ def test_import_apo_updates_provisional_curve_status_and_control_state(tmp_path) ] +def test_import_apo_preset_path_uses_autoeq_name_and_reveals_utility_pane() -> None: + calls: list[object] = [] + utility_button = FakeUtilityPaneButton(visible=True) + fake_window = SimpleNamespace( + controller=SimpleNamespace( + import_apo_preset=lambda path: calls.append(("import", path)) or 10, + state_signature=lambda: "imported-signature", + build_preset_payload=lambda label: {"name": label}, + ), + selected_band_index=3, + current_preset_name="Studio Reference", + saved_preset_signature="old-signature", + output_preset_auto_applied=True, + output_preset_curve_auto_loaded=True, + utility_pane_button=utility_button, + set_visible_band_count=lambda count: calls.append(("visible-bands", count)), + set_curve_revert_baseline=lambda label: calls.append(("baseline", label)), + refresh_preset_list=lambda: calls.append("presets"), + sync_ui_from_state=lambda: calls.append("sync"), + notify_control_state_changed=lambda: calls.append("notify"), + ) + + count = window.MiniEqWindow.import_apo_preset_path( + fake_window, + "/tmp/Example Reference Headphone ParametricEQ.txt", + imported_name="Example Reference Headphone", + ) + + assert count == 10 + assert fake_window.selected_band_index is None + assert fake_window.current_preset_name is None + assert fake_window.saved_preset_signature == "imported-signature" + assert fake_window.output_preset_auto_applied is False + assert fake_window.output_preset_curve_auto_loaded is False + assert utility_button.active is True + assert calls == [ + ("import", "/tmp/Example Reference Headphone ParametricEQ.txt"), + ("visible-bands", 10), + ("baseline", "Imported APO: Example Reference Headphone"), + "presets", + "sync", + "notify", + ] + + def test_on_bypass_changed_resets_switch_when_engine_update_fails() -> None: calls: list[object] = [] bypass_switch = FakeSwitch(False) diff --git a/tests/test_mini_eq_window_analyzer.py b/tests/test_mini_eq_window_analyzer.py index fb9658b..b7f7146 100644 --- a/tests/test_mini_eq_window_analyzer.py +++ b/tests/test_mini_eq_window_analyzer.py @@ -577,6 +577,13 @@ def test_loudness_summary_falls_back_while_shortterm_is_not_ready() -> None: assert window_analyzer.loudness_summary_lufs(snapshot) == "-21.0 LUFS" +def test_loudness_summary_treats_sub_floor_current_values_as_silence() -> None: + snapshot = analyzer.AnalyzerLoudnessSnapshot(-200.0, -190.0, -19.3) + + assert window_analyzer.loudness_current_lufs(snapshot) is None + assert window_analyzer.loudness_summary_lufs(snapshot) == "--" + + def test_visible_loudness_falls_back_while_shortterm_is_not_ready() -> None: window = AnalyzerSummaryWindow() window.analyzer_loudness_snapshot = analyzer.AnalyzerLoudnessSnapshot(-21.0, float("-inf"), float("-inf")) @@ -590,7 +597,21 @@ def test_visible_loudness_falls_back_while_shortterm_is_not_ready() -> None: assert window.analyzer_loudness_meter_area.accessible_description == "Current -21.0 LUFS · Peak --" +def test_visible_loudness_does_not_fall_back_to_integrated_after_silence() -> None: + window = AnalyzerSummaryWindow() + window.analyzer_loudness_snapshot = analyzer.AnalyzerLoudnessSnapshot(-200.0, -190.0, -19.3) + window.analyzer_session_max_shortterm_lufs = -19.3 + + window.update_analyzer_summary_label() + + assert window.analyzer_summary_label.text == "On · --" + assert window.analyzer_summary_label.tooltip == "Current -- · Peak -19.3 LUFS" + assert window.analyzer_loudness_value_label.text == "--" + assert window.analyzer_loudness_meter_area.accessible_description == "Current -- · Peak -19.3 LUFS" + + def test_loudness_session_max_ignores_silence() -> None: assert window_analyzer.update_loudness_max(None, float("-inf")) is None + assert window_analyzer.update_loudness_max(None, -200.0) is None assert window_analyzer.update_loudness_max(-18.0, -20.0) == pytest.approx(-18.0) assert window_analyzer.update_loudness_max(-18.0, -12.0) == pytest.approx(-12.0) diff --git a/tests/test_mini_eq_window_graph.py b/tests/test_mini_eq_window_graph.py index 15de3d2..f3d6649 100644 --- a/tests/test_mini_eq_window_graph.py +++ b/tests/test_mini_eq_window_graph.py @@ -47,6 +47,45 @@ def set_tooltip_text(self, text: str) -> None: self.tooltip = text +class FakeControl: + def __init__(self) -> None: + self.sensitive = True + self.visible = True + + def set_sensitive(self, sensitive: bool) -> None: + self.sensitive = sensitive + + def set_visible(self, visible: bool) -> None: + self.visible = visible + + +class FakeSpin(FakeControl): + def __init__(self) -> None: + super().__init__() + self.value = 0.0 + + def set_value(self, value: float) -> None: + self.value = value + + +class FakeDropDown(FakeControl): + def __init__(self) -> None: + super().__init__() + self.selected = 0 + + def set_selected(self, selected: int) -> None: + self.selected = selected + + +class FakeToggle(FakeControl): + def __init__(self) -> None: + super().__init__() + self.active = False + + def set_active(self, active: bool) -> None: + self.active = active + + class FakeScale: def __init__(self, value: float) -> None: self.value = value @@ -79,6 +118,47 @@ def __init__( self.inspector_summary_label = FakeLabel() +class SelectedBandEditorWindow(window_graph.MiniEqWindowGraphMixin): + def __init__(self, selected_band_index: int | None) -> None: + self.selected_band_index = selected_band_index + self.controller = SimpleNamespace( + bands=[ + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=2.5), + ] + ) + self.selected_band_label = FakeLabel() + self.selected_band_state_box = FakeControl() + self.selected_band_type_box = FakeControl() + self.selected_band_frequency_box = FakeControl() + self.selected_band_q_box = FakeControl() + self.selected_band_gain_box = FakeControl() + self.selected_band_type_combo = FakeDropDown() + self.selected_band_frequency_spin = FakeSpin() + self.selected_band_q_spin = FakeSpin() + self.selected_band_gain_spin = FakeSpin() + self.selected_band_mute_button = FakeToggle() + self.selected_band_solo_button = FakeToggle() + + def editor_groups(self) -> list[FakeControl]: + return [ + self.selected_band_state_box, + self.selected_band_type_box, + self.selected_band_frequency_box, + self.selected_band_q_box, + self.selected_band_gain_box, + ] + + def editor_controls(self) -> list[FakeControl]: + return [ + self.selected_band_type_combo, + self.selected_band_frequency_spin, + self.selected_band_q_spin, + self.selected_band_gain_spin, + self.selected_band_mute_button, + self.selected_band_solo_button, + ] + + def test_filter_type_label_handles_non_contiguous_filter_values() -> None: assert window_graph.filter_type_label(core.FILTER_TYPES["Allpass"]) == "Allpass" assert window_graph.filter_type_label(core.FILTER_TYPES["Bandpass"]) == "Bandpass" @@ -122,6 +202,41 @@ def test_focus_summary_handles_no_selected_band() -> None: assert window.inspector_summary_label.text == "No Band" +def test_selected_band_editor_keeps_parameter_space_visible_without_selection() -> None: + window = SelectedBandEditorWindow(selected_band_index=None) + window.selected_band_type_combo.selected = 1 + window.selected_band_frequency_spin.value = 640.0 + window.selected_band_q_spin.value = 1.4 + window.selected_band_gain_spin.value = -3.0 + window.selected_band_mute_button.active = True + window.selected_band_solo_button.active = True + + window.update_selected_band_editor() + + assert window.selected_band_label.text == "No Band" + assert window.selected_band_label.tooltip == "No band selected" + assert all(group.visible for group in window.editor_groups()) + assert all(not control.sensitive for control in window.editor_controls()) + assert window.selected_band_type_combo.selected == core.FILTER_TYPE_INDEX_BY_VALUE[core.FILTER_TYPES["Off"]] + assert window.selected_band_frequency_spin.value == window_graph.SELECTED_BAND_PLACEHOLDER_FREQUENCY_HZ + assert window.selected_band_q_spin.value == core.DEFAULT_BAND_Q + assert window.selected_band_gain_spin.value == 0.0 + assert window.selected_band_mute_button.active is False + assert window.selected_band_solo_button.active is False + + +def test_selected_band_editor_enables_parameter_controls_after_selection() -> None: + window = SelectedBandEditorWindow(selected_band_index=0) + + window.update_selected_band_editor() + + assert window.selected_band_label.text == "Band 1" + assert all(group.visible for group in window.editor_groups()) + assert all(control.sensitive for control in window.editor_controls()) + assert window.selected_band_frequency_spin.value == 1000.0 + assert window.selected_band_gain_spin.value == 2.5 + + def test_preamp_change_refreshes_preset_metadata() -> None: calls: list[object] = [] controller = SimpleNamespace(set_preamp_db=lambda value: calls.append(("preamp", value))) @@ -174,3 +289,20 @@ def test_curve_metadata_refresh_updates_preset_state_with_pending_idle(monkeypat assert calls == ["preset-state"] assert test_window.curve_metadata_refresh_source_id == 42 + + +def test_curve_metadata_refresh_idle_notifies_control_clients() -> None: + calls: list[str] = [] + test_window = SimpleNamespace( + curve_metadata_refresh_source_id=42, + ui_shutting_down=False, + update_status_summary=lambda: calls.append("status"), + update_preset_state=lambda: calls.append("preset-state"), + notify_control_state_changed=lambda: calls.append("control-state"), + ) + + keep_source = window_graph.MiniEqWindowGraphMixin.on_curve_metadata_refresh_idle(test_window) + + assert keep_source is False + assert test_window.curve_metadata_refresh_source_id == 0 + assert calls == ["status", "preset-state", "control-state"] diff --git a/tests/test_mini_eq_window_state.py b/tests/test_mini_eq_window_state.py new file mode 100644 index 0000000..90172c8 --- /dev/null +++ b/tests/test_mini_eq_window_state.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from tests._mini_eq_imports import import_mini_eq_module + +window_state = import_mini_eq_module("window_state") + + +class FakeSettings: + def __init__(self) -> None: + self.bindings: list[tuple[str, object, str, object]] = [] + + def bind(self, key: str, window: object, property_name: str, flags: object) -> None: + self.bindings.append((key, window, property_name, flags)) + + +def test_bind_window_state_binds_size_and_maximized(monkeypatch) -> None: + fake_settings = FakeSettings() + fake_window = object() + monkeypatch.setattr(window_state, "create_window_state_settings", lambda: fake_settings) + + assert window_state.bind_window_state(fake_window) is fake_settings + assert [(key, property_name) for key, _window, property_name, _flags in fake_settings.bindings] == [ + ("window-width", "default-width"), + ("window-height", "default-height"), + ("window-maximized", "maximized"), + ] + assert all(window is fake_window for _key, window, _property_name, _flags in fake_settings.bindings) + + +def test_bind_window_state_noops_when_schema_is_unavailable(monkeypatch) -> None: + monkeypatch.setattr(window_state, "create_window_state_settings", lambda: None) + + assert window_state.bind_window_state(object()) is None diff --git a/tests/test_release_preflight.py b/tests/test_release_preflight.py index 56ee577..c6e105a 100644 --- a/tests/test_release_preflight.py +++ b/tests/test_release_preflight.py @@ -23,6 +23,9 @@ def test_leak_pattern_matches_common_credential_prefixes() -> None: "value=" + "AK" + "IA" + ("A" * 16), "value=" + "AS" + "IA" + ("A" * 16), "api" + "_key=value", + "to" + "ken=" + ("A" * 40), + "sec" + "ret=" + ("A" * 40), + "sec" + "ret_key=" + ("A" * 40), "/" + "home/user/project", ] ) @@ -50,6 +53,12 @@ def test_allowed_matches_still_cover_public_release_references() -> None: assert release_preflight.allowed_leak_match(line) +def test_leak_pattern_ignores_regular_token_identifiers() -> None: + assert not leak_match("tokens = normalize_search_query(query)") + assert not leak_match("first_token = tokens[0]") + assert not leak_match("def score(tokens: list[str]) -> int:") + + def test_pipewire_gobject_build_environment_error_lists_missing_tools(monkeypatch) -> None: monkeypatch.setattr(release_preflight, "PIPEWIRE_GOBJECT_BUILD_TOOLS", ("definitely-missing-pwg-tool",)) monkeypatch.setattr(release_preflight, "PIPEWIRE_GOBJECT_PKG_CONFIG_MODULES", ()) @@ -97,6 +106,26 @@ def test_release_preflight_runs_headless_pipewire_runtime_smoke(monkeypatch) -> ] +def test_release_preflight_autoeq_live_notice_tracks_autoeq_paths(monkeypatch, capsys) -> None: + observed_paths: list[Path] = [] + + monkeypatch.setattr(release_preflight, "extension_comparison_base_tag", lambda: "v0.7.4") + + def changed_paths(_base_tag: str, paths: tuple[Path, ...]) -> list[str]: + observed_paths.extend(paths) + return ["src/mini_eq/autoeq.py"] + + monkeypatch.setattr(release_preflight, "changed_paths_for_review", changed_paths) + + release_preflight.run_autoeq_live_check_notice() + + output = capsys.readouterr().out + assert Path("src/mini_eq/autoeq.py") in observed_paths + assert Path("tools/check_autoeq_live.py") in observed_paths + assert "AutoEQ.app live compatibility check may be needed" in output + assert "python3 tools/check_autoeq_live.py" in output + + def test_release_preflight_uses_hosted_headless_pipewire_defaults(monkeypatch) -> None: commands: list[list[str | Path]] = [] diff --git a/tools/check_autoeq_live.py b/tools/check_autoeq_live.py new file mode 100644 index 0000000..34cc939 --- /dev/null +++ b/tools/check_autoeq_live.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +from mini_eq import autoeq # noqa: E402 +from mini_eq.core import EqBand, parse_apo_file # noqa: E402 + + +@dataclass(frozen=True) +class AutoEqLiveCheckResult: + entry: autoeq.AutoEqEntry + entry_count: int + target_count: int + preamp_db: float + band_count: int + + +def profile_description(name: str, source: str | None, form: str | None, rig: str | None) -> str: + parts = [part for part in (name, source, form) if part] + if rig: + parts.append(rig) + return " / ".join(parts) + + +def validate_entries(entries: list[autoeq.AutoEqEntry]) -> None: + if not entries: + raise RuntimeError("AutoEq profile list is empty after parsing") + + for entry in entries: + if not entry.name.strip() or not entry.source.strip() or not entry.form.strip(): + raise RuntimeError("AutoEq profile list includes an entry without name, source, or form") + + +def validate_targets(targets: list[object]) -> int: + if not targets: + raise RuntimeError("AutoEq target list is empty") + + labeled_targets = 0 + for target in targets: + if not isinstance(target, dict): + continue + label = str(target.get("label") or "").strip() + if label: + labeled_targets += 1 + + if labeled_targets == 0: + raise RuntimeError("AutoEq target list does not include any labeled targets") + return labeled_targets + + +def entry_matches_probe( + entry: autoeq.AutoEqEntry, + *, + name: str, + source: str | None, + form: str | None, + rig: str | None, +) -> bool: + if entry.name.casefold() != name.casefold(): + return False + if source and entry.source.casefold() != source.casefold(): + return False + if form and entry.form.casefold() != form.casefold(): + return False + return not rig or entry.rig.casefold() == rig.casefold() + + +def select_probe_entry( + entries: list[autoeq.AutoEqEntry], + *, + name: str | None, + source: str | None, + form: str | None, + rig: str | None, +) -> autoeq.AutoEqEntry: + if not entries: + raise RuntimeError("AutoEq profile list is empty after parsing") + + if name is None: + return entries[0] + + for entry in entries: + if entry_matches_probe(entry, name=name, source=source, form=form, rig=rig): + return entry + + candidates = autoeq.search_autoeq_entries(entries, name, limit=8) + if source: + candidates = [entry for entry in candidates if entry.source.casefold() == source.casefold()] + if form: + candidates = [entry for entry in candidates if entry.form.casefold() == form.casefold()] + if rig: + candidates = [entry for entry in candidates if entry.rig.casefold() == rig.casefold()] + + if candidates: + return candidates[0] + + sample = ", ".join( + profile_description(entry.name, entry.source, entry.form, entry.rig or None) + for entry in autoeq.search_autoeq_entries(entries, name, limit=5) + ) + if not sample: + sample = "no nearby profile matches" + + raise RuntimeError( + f"AutoEq profile probe could not find {profile_description(name, source, form, rig)}; nearest matches: {sample}" + ) + + +def parse_generated_apo(text: str) -> tuple[float, list[EqBand]]: + if "Preamp:" not in text or "Filter " not in text: + raise RuntimeError("AutoEq generated text does not look like an Equalizer APO preset") + + with tempfile.TemporaryDirectory(prefix="mini-eq-autoeq-live-") as temp_dir: + path = Path(temp_dir) / "autoeq-live.txt" + path.write_text(text, encoding="utf-8") + preamp_db, bands = parse_apo_file(str(path)) + + if not bands: + raise RuntimeError("Mini EQ APO parser did not import any AutoEq filters") + return preamp_db, bands + + +def check_autoeq_live( + *, + profile_name: str | None = None, + profile_source: str | None = None, + profile_form: str | None = None, + profile_rig: str | None = None, + timeout: int = autoeq.AUTOEQ_REQUEST_TIMEOUT_SECONDS, +) -> AutoEqLiveCheckResult: + entries_text = autoeq.fetch_text(autoeq.AUTOEQ_APP_ENTRIES_URL, timeout=timeout) + entries = autoeq.parse_autoeq_app_entries(entries_text) + validate_entries(entries) + + targets_text = autoeq.fetch_text(autoeq.AUTOEQ_APP_TARGETS_URL, timeout=timeout) + targets = autoeq.parse_autoeq_targets_data(targets_text) + target_count = validate_targets(targets) + + entry = select_probe_entry( + entries, + name=profile_name, + source=profile_source, + form=profile_form, + rig=profile_rig, + ) + data = autoeq.post_json( + autoeq.AUTOEQ_APP_EQUALIZE_URL, + autoeq.autoeq_equalize_body(entry, targets), + timeout=timeout, + ) + apo_text = autoeq.format_autoeq_parametric_eq(data.get("parametric_eq")) + preamp_db, bands = parse_generated_apo(apo_text) + + return AutoEqLiveCheckResult( + entry=entry, + entry_count=len(entries), + target_count=target_count, + preamp_db=preamp_db, + band_count=len(bands), + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Run a live AutoEQ.app compatibility smoke test.") + parser.add_argument( + "--profile", + default=None, + help="specific AutoEQ profile name to equalize; defaults to the first parsed profile", + ) + parser.add_argument("--source", default=None, help="expected AutoEQ measurement source when --profile is set") + parser.add_argument("--form", default=None, help="expected AutoEQ profile form when --profile is set") + parser.add_argument("--rig", default=None, help="optional expected AutoEQ measurement rig") + parser.add_argument( + "--timeout", + type=int, + default=autoeq.AUTOEQ_REQUEST_TIMEOUT_SECONDS, + help="network timeout in seconds for each AutoEQ request", + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if args.timeout <= 0: + parser.error("--timeout must be greater than zero") + + try: + result = check_autoeq_live( + profile_name=args.profile, + profile_source=args.source, + profile_form=args.form, + profile_rig=args.rig, + timeout=args.timeout, + ) + except Exception as exc: + print(f"AutoEQ live check failed: {exc}", file=sys.stderr) + return 1 + + print("AutoEQ live check passed.") + print(f"Profiles parsed: {result.entry_count}") + print(f"Targets parsed: {result.target_count}") + print( + "Probe profile: " + f"{profile_description(result.entry.name, result.entry.source, result.entry.form, result.entry.rig or None)}" + ) + print(f"Imported APO filters: {result.band_count}") + print(f"Imported preamp: {result.preamp_db:.2f} dB") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/gnome-shell-extension/fake_mini_eq_control.py b/tools/gnome-shell-extension/fake_mini_eq_control.py index f2f19e6..5c69a51 100755 --- a/tools/gnome-shell-extension/fake_mini_eq_control.py +++ b/tools/gnome-shell-extension/fake_mini_eq_control.py @@ -119,8 +119,13 @@ def state(self) -> dict[str, GLib.Variant]: "eq_enabled": GLib.Variant("b", self.eq_enabled), "routed": GLib.Variant("b", self.routed), "preset_name": GLib.Variant("s", self.preset_name), + "curve_name": GLib.Variant("s", self.preset_name), + "curve_status": GLib.Variant("s", "preset"), + "curve_label": GLib.Variant("s", self.preset_name), "output_sink": GLib.Variant("s", "Demo Output"), "output_preset_name": GLib.Variant("s", self.output_preset_name), + "output_preset_status": GLib.Variant("s", "applied"), + "output_preset_label": GLib.Variant("s", f"Applied - {self.output_preset_name}"), "output_preset_auto_applied": GLib.Variant("b", True), "analyzer_enabled": GLib.Variant("b", self.analyzer_enabled), "background_mode": GLib.Variant("b", True), diff --git a/tools/prepare_release.py b/tools/prepare_release.py index 2258bf2..00e0c59 100755 --- a/tools/prepare_release.py +++ b/tools/prepare_release.py @@ -154,7 +154,10 @@ def main() -> int: for edit in changed: edit.path.write_text(edit.after, encoding="utf-8") print(f"updated {edit.path}") - print("Run tests/test_version_metadata.py and review the generated release notes before committing.") + print( + "Run tests/test_version_metadata.py and review the generated release notes before committing. " + "They are shown in AppStream and the About dialog." + ) return 0 diff --git a/tools/release_preflight.py b/tools/release_preflight.py index 630d932..d992592 100755 --- a/tools/release_preflight.py +++ b/tools/release_preflight.py @@ -19,7 +19,8 @@ ROOT = Path(__file__).resolve().parents[1] LEAK_PATTERN = ( - r"(/home/|/Users/|secret|token|api[_-]?key|github_pat|" + r"(/home/|/Users/|(^|[^A-Za-z0-9_])secret[ \t]*[:=]|(^|[^A-Za-z0-9_])secret[_-]?key|" + r"(^|[^A-Za-z0-9_])token[ \t]*=|(^|[^A-Za-z0-9_])token[_-]?value[ \t]*=|api[_-]?key|github_pat|" r"gh[pousr]_[A-Za-z0-9_]{20,}|pypi-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{20,}|" r"AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|BEGIN [A-Z0-9 ]*PRIVATE KEY)" ) @@ -51,6 +52,13 @@ Path("src/mini_eq/window_preferences.py"), Path("extensions/gnome-shell/mini-eq@bhack.github.io/extension.js"), ) +AUTOEQ_LIVE_REVIEW_PATHS = ( + Path(".github/workflows/autoeq-live.yml"), + Path("src/mini_eq/autoeq.py"), + Path("src/mini_eq/window_autoeq.py"), + Path("tests/test_mini_eq_autoeq.py"), + Path("tools/check_autoeq_live.py"), +) PIPEWIRE_GOBJECT_BUILD_TOOLS = ("g-ir-compiler", "g-ir-scanner", "pkg-config") PIPEWIRE_GOBJECT_PKG_CONFIG_MODULES = ("glib-2.0", "gio-2.0", "gobject-2.0", "libpipewire-0.3") PIPEWIRE_GOBJECT_DEBIAN_BUILD_PACKAGES = ( @@ -248,6 +256,26 @@ def run_background_portal_smoke_notice() -> None: print("Run one clean-permission Flatpak portal smoke in a real GNOME session before releasing this change.") +def run_autoeq_live_check_notice() -> None: + base_tag = extension_comparison_base_tag() + if base_tag is None: + print("\nAutoEQ.app live compatibility notice skipped; no release tag found.") + return + + changes = changed_paths_for_review(base_tag, AUTOEQ_LIVE_REVIEW_PATHS) + if not changes: + print(f"\nAutoEQ.app live compatibility check not indicated; AutoEQ integration unchanged since {base_tag}.") + return + + print(f"\nAutoEQ.app live compatibility check may be needed; AutoEQ integration changed since {base_tag}:") + for path in changes: + print(f" {path}") + print( + "Run python3 tools/check_autoeq_live.py before release. A failure is a live-service or format-drift " + "signal to investigate, not an automatic blocker for unrelated fixes." + ) + + def run_flatpak_runtime_smoke_notice() -> None: base_tag = extension_comparison_base_tag() if base_tag is None: @@ -483,6 +511,7 @@ def main() -> int: run_flatpak_runtime_smoke_notice() run_live_ui_runtime_smoke_notice() run_background_portal_smoke_notice() + run_autoeq_live_check_notice() run([python, "-m", "ruff", "check", "."]) run([python, "-m", "ruff", "format", "--check", "."]) run([python, "-m", "pytest", "-q"]) diff --git a/tools/render_demo_screenshot.py b/tools/render_demo_screenshot.py index 2ae9d3a..4401d34 100644 --- a/tools/render_demo_screenshot.py +++ b/tools/render_demo_screenshot.py @@ -114,7 +114,7 @@ def do_activate(self) -> None: self.window.refresh_preset_list() self.window.set_visible(True) self.window.present() - self.window.schedule_post_present_setup() + self.window.schedule_startup_ready() GLib.timeout_add(self.delay_ms, self.on_capture_timeout) def on_capture_timeout(self) -> bool: