diff --git a/API.md b/API.md index ad89365b..510674c6 100644 --- a/API.md +++ b/API.md @@ -1510,7 +1510,8 @@ or an error: "name": "condition_name", "filter": "...", "cli": "...", - ... + "extras": {}, + "enabled": true }, ... ] @@ -1529,6 +1530,8 @@ or an error: "name": "Use proxy for region locked content", "filter": "availability = 'needs_auth' & channel_id = 'channel_id'", "cli": "--proxy http://myproxy.com:8080", + "extras": {}, + "enabled": true }, ... ] @@ -1542,11 +1545,17 @@ or an error: "name": "Use proxy for region locked content", "filter": "availability = 'needs_auth' & channel_id = 'channel_id'", "cli": "--proxy http://myproxy.com:8080", + "extras": {}, + "enabled": true }, ... ] ``` +**Notes**: +- Disabled conditions (`enabled: false`) will be stored but ignored during matching. +- All conditions are enabled by default when the `enabled` field is not provided. + --- ### POST /api/conditions/test diff --git a/app/library/conditions.py b/app/library/conditions.py index cdc9e954..118b90e1 100644 --- a/app/library/conditions.py +++ b/app/library/conditions.py @@ -34,6 +34,9 @@ class Condition: extras: dict[str, Any] = field(default_factory=dict) """Any extra data to store with the condition.""" + enabled: bool = True + """Whether the condition is enabled.""" + def serialize(self) -> dict: return self.__dict__ @@ -138,6 +141,10 @@ class Conditions(metaclass=Singleton): item["extras"] = {} need_save = True + if "enabled" not in item: + item["enabled"] = True + need_save = True + item: Condition = init_class(Condition, item) self._items.append(item) @@ -146,7 +153,7 @@ class Conditions(metaclass=Singleton): continue if need_save: - LOG.info("Saving conditions due changes.") + LOG.warning("Saving conditions due to schema changes.") self.save(self._items) return self @@ -299,6 +306,9 @@ class Conditions(metaclass=Singleton): return None for item in self.get_all(): + if not item.enabled: + continue + if not item.filter: LOG.error(f"Filter is empty for '{item.name}'.") continue @@ -330,7 +340,7 @@ class Conditions(metaclass=Singleton): return None item = self.get(name) - if not item or not item.filter: + if not item or not item.enabled or not item.filter: return None return item if match_str(item.filter, info) else None diff --git a/app/routes/api/conditions.py b/app/routes/api/conditions.py index a5102701..3aef2412 100644 --- a/app/routes/api/conditions.py +++ b/app/routes/api/conditions.py @@ -94,6 +94,9 @@ async def conditions_add(request: Request, encoder: Encoder, notify: EventBus) - if not item.get("extras"): item["extras"] = {} + if "enabled" not in item: + item["enabled"] = True + if not item.get("id", None) or not validate_uuid(item.get("id"), version=4): item["id"] = str(uuid.uuid4()) diff --git a/app/schema/conditions.json b/app/schema/conditions.json index 7ebcb3ac..010d8444 100644 --- a/app/schema/conditions.json +++ b/app/schema/conditions.json @@ -34,6 +34,11 @@ "type": "object", "description": "Any extra data to store with the condition.", "default": {} + }, + "enabled": { + "type": "boolean", + "description": "Whether the condition is enabled.", + "default": true } }, "examples": [ @@ -43,7 +48,8 @@ "filter": "duration>3600", "extras": { "ignore_download": true - } + }, + "enabled": true }, { "id": "b3c4d5e6-7890-3456-1cde-f23456789012", @@ -51,7 +57,8 @@ "filter": "categories~='Music'", "extras": { "set_preset": "audio-only" - } + }, + "enabled": false } ] } diff --git a/app/tests/test_conditions.py b/app/tests/test_conditions.py index 790bf48d..ca0ce03c 100644 --- a/app/tests/test_conditions.py +++ b/app/tests/test_conditions.py @@ -41,6 +41,7 @@ class TestCondition: # Check defaults assert condition.cli == "" assert condition.extras == {} + assert condition.enabled is True def test_condition_creation_with_all_fields(self): """Test creating a condition with all fields specified.""" @@ -48,7 +49,7 @@ class TestCondition: extras = {"key": "value", "number": 42} condition = Condition( - id=test_id, name="full_test", filter="uploader = 'test'", cli="--format best", extras=extras + id=test_id, name="full_test", filter="uploader = 'test'", cli="--format best", extras=extras, enabled=False ) assert condition.id == test_id @@ -56,6 +57,7 @@ class TestCondition: assert condition.filter == "uploader = 'test'" assert condition.cli == "--format best" assert condition.extras == extras + assert condition.enabled is False def test_condition_serialize(self): """Test condition serialization to dict.""" @@ -209,6 +211,7 @@ class TestConditions: "filter": "duration < 300", "cli": "--format worst", "extras": {"category": "short"}, + "enabled": True, }, { "id": str(uuid.uuid4()), @@ -216,6 +219,7 @@ class TestConditions: "filter": "title ~= 'music'", "cli": "--audio-quality 0", "extras": {"type": "audio"}, + "enabled": False, }, ] @@ -232,10 +236,12 @@ class TestConditions: assert conditions._items[0].filter == "duration < 300" assert conditions._items[0].cli == "--format worst" assert conditions._items[0].extras == {"category": "short"} + assert conditions._items[0].enabled is True # Check second condition assert conditions._items[1].name == "music_videos" assert conditions._items[1].filter == "title ~= 'music'" + assert conditions._items[1].enabled is False def test_load_conditions_without_id(self): """Test loading conditions that don't have ID (should generate ID).""" @@ -279,6 +285,26 @@ class TestConditions: # Should call save due to changes mock_save.assert_called_once() + def test_load_conditions_without_enabled(self): + """Test loading conditions that don't have enabled field (should default to True).""" + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "no_enabled_conditions.json" + + test_data = [{"id": str(uuid.uuid4()), "name": "no_enabled_test", "filter": "duration > 60", "extras": {}}] + + file_path.write_text(json.dumps(test_data)) + + with patch.object(Conditions, "save") as mock_save: + conditions = Conditions(file=file_path) + conditions.load() + + # Should have generated enabled=True + assert len(conditions._items) == 1 + assert conditions._items[0].enabled is True + + # Should call save due to changes + mock_save.assert_called_once() + def test_load_invalid_json(self): """Test loading file with invalid JSON.""" with tempfile.TemporaryDirectory() as temp_dir: @@ -613,6 +639,28 @@ class TestConditions: assert result is None + @patch("app.library.conditions.match_str") + def test_match_disabled_condition(self, mock_match_str): + """Test that disabled conditions are skipped during matching.""" + mock_match_str.return_value = True + + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "disabled_match_test.json" + conditions = Conditions(file=file_path) + + # Add disabled condition + disabled_condition = Condition(name="disabled_test", filter="duration > 60", enabled=False) + enabled_condition = Condition(name="enabled_test", filter="duration > 120", enabled=True) + conditions._items = [disabled_condition, enabled_condition] + + info_dict = {"duration": 150} + result = conditions.match(info_dict) + + # Should skip disabled condition and match enabled one + assert result is enabled_condition + # Should only call match_str once for enabled condition + mock_match_str.assert_called_once_with("duration > 120", info_dict) + def test_match_empty_filter(self): """Test matching with condition that has empty filter.""" with tempfile.TemporaryDirectory() as temp_dir: @@ -687,6 +735,21 @@ class TestConditions: assert result is None + def test_single_match_disabled_condition(self): + """Test single matching with disabled condition.""" + with tempfile.TemporaryDirectory() as temp_dir: + file_path = Path(temp_dir) / "disabled_single_test.json" + conditions = Conditions(file=file_path) + + test_condition = Condition(name="disabled_single", filter="duration > 60", enabled=False) + conditions._items = [test_condition] + + info_dict = {"duration": 120} + result = conditions.single_match("disabled_single", info_dict) + + # Should return None because condition is disabled + assert result is None + def test_single_match_invalid_inputs(self): """Test single matching with invalid inputs.""" with tempfile.TemporaryDirectory() as temp_dir: diff --git a/ui/app/components/ConditionForm.vue b/ui/app/components/ConditionForm.vue index 15c9d5f8..dc926963 100644 --- a/ui/app/components/ConditionForm.vue +++ b/ui/app/components/ConditionForm.vue @@ -49,7 +49,7 @@ -
+
+
+
+ +
+ + +
+ + + Whether the condition is enabled. + +
+
+