From 4354c0974cf55cd9c709dfdb6f56ffe6d85a5b79 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 16 Dec 2025 15:09:00 +0000 Subject: [PATCH 1/5] Add get_torrents service and me as codeowner --- CODEOWNERS | 4 +- .../components/transmission/const.py | 14 ++++ .../components/transmission/helpers.py | 45 +++++++++++ .../components/transmission/icons.json | 3 + .../components/transmission/manifest.json | 2 +- .../components/transmission/sensor.py | 41 +++------- .../components/transmission/services.py | 58 ++++++++++++- .../components/transmission/services.yaml | 21 +++++ .../components/transmission/strings.json | 23 ++++++ tests/components/transmission/conftest.py | 13 ++- .../components/transmission/test_services.py | 81 +++++++++++++++++++ 11 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/transmission/helpers.py diff --git a/CODEOWNERS b/CODEOWNERS index 093fe2f0e68591..f64102d10e037b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1692,8 +1692,8 @@ build.json @home-assistant/supervisor /tests/components/trafikverket_train/ @gjohansson-ST /homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST /tests/components/trafikverket_weatherstation/ @gjohansson-ST -/homeassistant/components/transmission/ @engrbm87 @JPHutchins -/tests/components/transmission/ @engrbm87 @JPHutchins +/homeassistant/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp +/tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp /homeassistant/components/trend/ @jpbede /tests/components/trend/ @jpbede /homeassistant/components/triggercmd/ @rvmey diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index c232f26cefd127..da4cc6ed5c8149 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -40,9 +40,12 @@ ATTR_DELETE_DATA = "delete_data" ATTR_TORRENT = "torrent" +ATTR_TORRENTS = "torrents" ATTR_DOWNLOAD_PATH = "download_path" +ATTR_TORRENT_FILTER = "torrent_filter" SERVICE_ADD_TORRENT = "add_torrent" +SERVICE_GET_TORRENTS = "get_torrents" SERVICE_REMOVE_TORRENT = "remove_torrent" SERVICE_START_TORRENT = "start_torrent" SERVICE_STOP_TORRENT = "stop_torrent" @@ -54,3 +57,14 @@ STATE_UP_DOWN = "up_down" STATE_SEEDING = "seeding" STATE_DOWNLOADING = "downloading" + +FILTER_MODES: dict[str, list[str] | None] = { + "started": ["downloading"], + "completed": ["seeding"], + "paused": ["stopped"], + "active": [ + "seeding", + "downloading", + ], + "all": None, +} diff --git a/homeassistant/components/transmission/helpers.py b/homeassistant/components/transmission/helpers.py new file mode 100644 index 00000000000000..4a3ddc28b27a95 --- /dev/null +++ b/homeassistant/components/transmission/helpers.py @@ -0,0 +1,45 @@ +"""Helper functions for Transmission.""" + +from typing import Any + +from transmission_rpc.torrent import Torrent + + +def format_torrent(torrent: Torrent) -> dict[str, Any]: + """Format a single torrent.""" + value: dict[str, Any] = {} + + value["id"] = torrent.id + value["name"] = torrent.name + value["status"] = torrent.status.value + value["percent_done"] = f"{torrent.percent_done * 100:.2f}%" + value["ratio"] = f"{torrent.ratio:.2f}" + value["eta"] = str(torrent.eta) if torrent.eta else None + value["added_date"] = torrent.added_date.isoformat() + value["done_date"] = torrent.done_date.isoformat() if torrent.done_date else None + value["download_dir"] = torrent.download_dir + value["labels"] = torrent.labels + + return value + + +def filter_torrents( + torrents: list[Torrent], statuses: list[str] | None = None +) -> list[Torrent]: + """Filter torrents based on the statuses provided.""" + return [ + torrent + for torrent in torrents + if statuses is None or torrent.status in statuses + ] + + +def format_torrents( + torrents: list[Torrent], +) -> dict[str, dict[str, Any]]: + """Format a list of torrents.""" + value = {} + for torrent in torrents: + value[torrent.name] = format_torrent(torrent) + + return value diff --git a/homeassistant/components/transmission/icons.json b/homeassistant/components/transmission/icons.json index 287f9f501b0db9..20b296e9fc08a2 100644 --- a/homeassistant/components/transmission/icons.json +++ b/homeassistant/components/transmission/icons.json @@ -42,6 +42,9 @@ "add_torrent": { "service": "mdi:download" }, + "get_torrents": { + "service": "mdi:file-arrow-up-down-outline" + }, "remove_torrent": { "service": "mdi:download-off" }, diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index 69ed258f511d7a..6c6d18517dbd04 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -1,7 +1,7 @@ { "domain": "transmission", "name": "Transmission", - "codeowners": ["@engrbm87", "@JPHutchins"], + "codeowners": ["@engrbm87", "@JPHutchins", "@andrew-codechimp"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/transmission", "integration_type": "service", diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index f6a0c0f9066239..adf778c0158609 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -7,8 +7,6 @@ from dataclasses import dataclass from typing import Any -from transmission_rpc.torrent import Torrent - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -20,6 +18,7 @@ from homeassistant.helpers.typing import StateType from .const import ( + FILTER_MODES, STATE_ATTR_TORRENT_INFO, STATE_DOWNLOADING, STATE_SEEDING, @@ -28,20 +27,10 @@ ) from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator from .entity import TransmissionEntity +from .helpers import filter_torrents PARALLEL_UPDATES = 0 -MODES: dict[str, list[str] | None] = { - "started_torrents": ["downloading"], - "completed_torrents": ["seeding"], - "paused_torrents": ["stopped"], - "active_torrents": [ - "seeding", - "downloading", - ], - "total_torrents": None, -} - @dataclass(frozen=True, kw_only=True) class TransmissionSensorEntityDescription(SensorEntityDescription): @@ -84,7 +73,7 @@ class TransmissionSensorEntityDescription(SensorEntityDescription): translation_key="active_torrents", val_func=lambda coordinator: coordinator.data.active_torrent_count, extra_state_attr_func=lambda coordinator: _torrents_info_attr( - coordinator=coordinator, key="active_torrents" + coordinator=coordinator, key="active" ), ), TransmissionSensorEntityDescription( @@ -92,7 +81,7 @@ class TransmissionSensorEntityDescription(SensorEntityDescription): translation_key="paused_torrents", val_func=lambda coordinator: coordinator.data.paused_torrent_count, extra_state_attr_func=lambda coordinator: _torrents_info_attr( - coordinator=coordinator, key="paused_torrents" + coordinator=coordinator, key="paused" ), ), TransmissionSensorEntityDescription( @@ -100,27 +89,27 @@ class TransmissionSensorEntityDescription(SensorEntityDescription): translation_key="total_torrents", val_func=lambda coordinator: coordinator.data.torrent_count, extra_state_attr_func=lambda coordinator: _torrents_info_attr( - coordinator=coordinator, key="total_torrents" + coordinator=coordinator, key="total" ), ), TransmissionSensorEntityDescription( key="completed_torrents", translation_key="completed_torrents", val_func=lambda coordinator: len( - _filter_torrents(coordinator.torrents, MODES["completed_torrents"]) + filter_torrents(coordinator.torrents, FILTER_MODES["completed"]) ), extra_state_attr_func=lambda coordinator: _torrents_info_attr( - coordinator=coordinator, key="completed_torrents" + coordinator=coordinator, key="completed" ), ), TransmissionSensorEntityDescription( key="started_torrents", translation_key="started_torrents", val_func=lambda coordinator: len( - _filter_torrents(coordinator.torrents, MODES["started_torrents"]) + filter_torrents(coordinator.torrents, FILTER_MODES["started"]) ), extra_state_attr_func=lambda coordinator: _torrents_info_attr( - coordinator=coordinator, key="started_torrents" + coordinator=coordinator, key="started" ), ), ) @@ -169,21 +158,11 @@ def get_state(upload: int, download: int) -> str: return STATE_IDLE -def _filter_torrents( - torrents: list[Torrent], statuses: list[str] | None = None -) -> list[Torrent]: - return [ - torrent - for torrent in torrents - if statuses is None or torrent.status in statuses - ] - - def _torrents_info_attr( coordinator: TransmissionDataUpdateCoordinator, key: str ) -> dict[str, Any]: infos = {} - torrents = _filter_torrents(coordinator.torrents, MODES[key]) + torrents = filter_torrents(coordinator.torrents, FILTER_MODES.get(key)) torrents = SUPPORTED_ORDER_MODES[coordinator.order](torrents) for torrent in torrents[: coordinator.limit]: info = infos[torrent.name] = { diff --git a/homeassistant/components/transmission/services.py b/homeassistant/components/transmission/services.py index ff03583e470f43..bcce2e10c15643 100644 --- a/homeassistant/components/transmission/services.py +++ b/homeassistant/components/transmission/services.py @@ -1,14 +1,16 @@ """Define services for the Transmission integration.""" +from enum import StrEnum from functools import partial import logging -from typing import cast +from typing import Any, cast +from transmission_rpc import Torrent import voluptuous as vol from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ID -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, selector @@ -16,18 +18,34 @@ ATTR_DELETE_DATA, ATTR_DOWNLOAD_PATH, ATTR_TORRENT, + ATTR_TORRENT_FILTER, + ATTR_TORRENTS, CONF_ENTRY_ID, DEFAULT_DELETE_DATA, DOMAIN, + FILTER_MODES, SERVICE_ADD_TORRENT, + SERVICE_GET_TORRENTS, SERVICE_REMOVE_TORRENT, SERVICE_START_TORRENT, SERVICE_STOP_TORRENT, ) from .coordinator import TransmissionDataUpdateCoordinator +from .helpers import filter_torrents, format_torrents _LOGGER = logging.getLogger(__name__) + +class TorrentFilter(StrEnum): + """TorrentFilter model.""" + + ALL = "all" + STARTED = "started" + COMPLETED = "completed" + PAUSED = "paused" + ACTIVE = "active" + + SERVICE_BASE_SCHEMA = vol.Schema( { vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector( @@ -45,6 +63,16 @@ ), ) +SERVICE_GET_TORRENTS_SCHEMA = vol.All( + SERVICE_BASE_SCHEMA.extend( + { + vol.Required(ATTR_TORRENT_FILTER): vol.In( + [x.lower() for x in TorrentFilter] + ), + } + ), +) + SERVICE_REMOVE_TORRENT_SCHEMA = vol.All( SERVICE_BASE_SCHEMA.extend( { @@ -111,6 +139,24 @@ async def _async_add_torrent(service: ServiceCall) -> None: await coordinator.async_request_refresh() +async def _async_get_torrents(service: ServiceCall) -> dict[str, Any] | None: + """Get torrents.""" + coordinator = _get_coordinator_from_service_data(service) + torrent_filter: str = service.data[ATTR_TORRENT_FILTER] + + def get_filtered_torrents() -> list[Torrent]: + """Filter torrents based on the filter provided.""" + all_torrents = coordinator.api.get_torrents() + return filter_torrents(all_torrents, FILTER_MODES[torrent_filter]) + + torrents = await service.hass.async_add_executor_job(get_filtered_torrents) + + info = format_torrents(torrents) + return { + ATTR_TORRENTS: info, + } + + async def _async_start_torrent(service: ServiceCall) -> None: """Start torrent.""" coordinator = _get_coordinator_from_service_data(service) @@ -149,6 +195,14 @@ def async_setup_services(hass: HomeAssistant) -> None: schema=SERVICE_ADD_TORRENT_SCHEMA, ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_TORRENTS, + _async_get_torrents, + schema=SERVICE_GET_TORRENTS_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( DOMAIN, SERVICE_REMOVE_TORRENT, diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index cadfbee2f63830..3afc870337ee44 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -16,6 +16,27 @@ add_torrent: selector: text: +get_torrents: + fields: + entry_id: + required: true + selector: + config_entry: + integration: transmission + torrent_filter: + required: true + example: "all" + default: "all" + selector: + select: + options: + - "all" + - "active" + - "started" + - "paused" + - "completed" + translation_key: torrent_filter + remove_torrent: fields: entry_id: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 903f48885ea51c..aeeaabad3ba485 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -120,6 +120,15 @@ "oldest_first": "Oldest first", "worst_ratio_first": "Worst ratio first" } + }, + "torrent_filter": { + "options": { + "active": "Active", + "all": "All", + "completed": "Completed (Seeding)", + "paused": "Paused", + "started": "Started (Downloading)" + } } }, "services": { @@ -141,6 +150,20 @@ }, "name": "Add torrent" }, + "get_torrents": { + "description": "Gets a list of current torrents", + "fields": { + "entry_id": { + "description": "[%key:component::transmission::services::add_torrent::fields::entry_id::description%]", + "name": "[%key:component::transmission::services::add_torrent::fields::entry_id::name%]" + }, + "torrent_filter": { + "description": "What kind of torrents you want to return, such as All or Active.", + "name": "Torrent filter" + } + }, + "name": "Get torrents" + }, "remove_torrent": { "description": "Removes a torrent.", "fields": { diff --git a/tests/components/transmission/conftest.py b/tests/components/transmission/conftest.py index 0390981db92354..2adb1bf67b2922 100644 --- a/tests/components/transmission/conftest.py +++ b/tests/components/transmission/conftest.py @@ -87,16 +87,15 @@ def _create_mock_torrent( torrent_data = { "id": torrent_id, "name": name, - "percentDone": percent_done, "status": status, - "rateDownload": 0, - "rateUpload": 0, - "downloadDir": download_dir, + "percentDone": percent_done, + "uploadRatio": ratio, + "ratio": ratio, "eta": eta, "addedDate": int(added_date.timestamp()), - "uploadRatio": ratio, - "error": 0, - "errorString": "", + "doneDate": int(added_date.timestamp()) if percent_done >= 1.0 else 0, + "downloadDir": download_dir, + "labels": [], } return Torrent(fields=torrent_data) diff --git a/tests/components/transmission/test_services.py b/tests/components/transmission/test_services.py index 45061e7b30a9fa..dcc9a51efa16fc 100644 --- a/tests/components/transmission/test_services.py +++ b/tests/components/transmission/test_services.py @@ -8,9 +8,12 @@ ATTR_DELETE_DATA, ATTR_DOWNLOAD_PATH, ATTR_TORRENT, + ATTR_TORRENT_FILTER, + ATTR_TORRENTS, CONF_ENTRY_ID, DOMAIN, SERVICE_ADD_TORRENT, + SERVICE_GET_TORRENTS, SERVICE_REMOVE_TORRENT, SERVICE_START_TORRENT, SERVICE_STOP_TORRENT, @@ -252,3 +255,81 @@ async def test_remove_torrent_service_with_delete_data( ) client.remove_torrent.assert_called_once_with(789, delete_data=True) + + +@pytest.mark.parametrize( + ("filter_mode", "expected_statuses"), + [ + ("all", None), + ("started", ["downloading"]), + ("completed", ["seeding"]), + ("paused", ["stopped"]), + ("active", ["seeding", "downloading"]), + ], +) +async def test_get_torrents_service( + hass: HomeAssistant, + mock_transmission_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_torrent, + filter_mode: str, + expected_statuses: list[str] | None, +) -> None: + """Test get torrents service with various filter modes.""" + client = mock_transmission_client.return_value + + downloading_torrent = mock_torrent(torrent_id=1, name="Downloading", status=4) + seeding_torrent = mock_torrent(torrent_id=2, name="Seeding", status=6) + stopped_torrent = mock_torrent(torrent_id=3, name="Stopped", status=0) + + client.get_torrents.return_value = [ + downloading_torrent, + seeding_torrent, + stopped_torrent, + ] + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_TORRENTS, + { + CONF_ENTRY_ID: mock_config_entry.entry_id, + ATTR_TORRENT_FILTER: filter_mode, + }, + blocking=True, + return_response=True, + ) + + assert response is not None + assert ATTR_TORRENTS in response + torrents = response[ATTR_TORRENTS] + assert isinstance(torrents, dict) + + if filter_mode == "all": + assert len(torrents) == 3 + assert "Downloading" in torrents + assert "Seeding" in torrents + assert "Stopped" in torrents + elif filter_mode == "started": + assert len(torrents) == 1 + assert "Downloading" in torrents + elif filter_mode == "completed": + assert len(torrents) == 1 + assert "Seeding" in torrents + elif filter_mode == "paused": + assert len(torrents) == 1 + assert "Stopped" in torrents + elif filter_mode == "active": + assert len(torrents) == 2 + assert "Downloading" in torrents + assert "Seeding" in torrents + + for torrent_name, torrent_data in torrents.items(): + assert isinstance(torrent_data, dict) + assert "id" in torrent_data + assert "name" in torrent_data + assert "status" in torrent_data + assert torrent_data["name"] == torrent_name From 7b30af8ff25c2cbad712c41d305d1f8e866c281c Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 16 Dec 2025 15:22:31 +0000 Subject: [PATCH 2/5] Remove if from test --- .../components/transmission/test_services.py | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/components/transmission/test_services.py b/tests/components/transmission/test_services.py index dcc9a51efa16fc..b4a585076cd454 100644 --- a/tests/components/transmission/test_services.py +++ b/tests/components/transmission/test_services.py @@ -260,7 +260,7 @@ async def test_remove_torrent_service_with_delete_data( @pytest.mark.parametrize( ("filter_mode", "expected_statuses"), [ - ("all", None), + ("all", ["seeding", "downloading", "stopped"]), ("started", ["downloading"]), ("completed", ["seeding"]), ("paused", ["stopped"]), @@ -273,7 +273,7 @@ async def test_get_torrents_service( mock_config_entry: MockConfigEntry, mock_torrent, filter_mode: str, - expected_statuses: list[str] | None, + expected_statuses: list[str], ) -> None: """Test get torrents service with various filter modes.""" client = mock_transmission_client.return_value @@ -308,24 +308,7 @@ async def test_get_torrents_service( torrents = response[ATTR_TORRENTS] assert isinstance(torrents, dict) - if filter_mode == "all": - assert len(torrents) == 3 - assert "Downloading" in torrents - assert "Seeding" in torrents - assert "Stopped" in torrents - elif filter_mode == "started": - assert len(torrents) == 1 - assert "Downloading" in torrents - elif filter_mode == "completed": - assert len(torrents) == 1 - assert "Seeding" in torrents - elif filter_mode == "paused": - assert len(torrents) == 1 - assert "Stopped" in torrents - elif filter_mode == "active": - assert len(torrents) == 2 - assert "Downloading" in torrents - assert "Seeding" in torrents + assert len(torrents) == len(expected_statuses) for torrent_name, torrent_data in torrents.items(): assert isinstance(torrent_data, dict) From b9ceb237dee001669016b393f074764cdefe98c9 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 16 Dec 2025 16:15:21 +0000 Subject: [PATCH 3/5] Remove misleading explanations --- homeassistant/components/transmission/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index aeeaabad3ba485..8cda05f98f444c 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -125,9 +125,9 @@ "options": { "active": "Active", "all": "All", - "completed": "Completed (Seeding)", + "completed": "Completed", "paused": "Paused", - "started": "Started (Downloading)" + "started": "Started" } } }, From 4ddf309f06830b19ec92df1074cd4e396eea54a1 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 16 Dec 2025 16:24:58 +0000 Subject: [PATCH 4/5] Update homeassistant/components/transmission/strings.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/transmission/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 8cda05f98f444c..6eeadb3dca285a 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -151,7 +151,7 @@ "name": "Add torrent" }, "get_torrents": { - "description": "Gets a list of current torrents", + "description": "Get a list of current torrents", "fields": { "entry_id": { "description": "[%key:component::transmission::services::add_torrent::fields::entry_id::description%]", From 29c2580aa9885dec94d7a5b2dc315f1e247913fb Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 16 Dec 2025 16:36:03 +0000 Subject: [PATCH 5/5] Add assert that torrents are in filter --- tests/components/transmission/test_services.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/components/transmission/test_services.py b/tests/components/transmission/test_services.py index b4a585076cd454..8a9031743aacb1 100644 --- a/tests/components/transmission/test_services.py +++ b/tests/components/transmission/test_services.py @@ -258,13 +258,13 @@ async def test_remove_torrent_service_with_delete_data( @pytest.mark.parametrize( - ("filter_mode", "expected_statuses"), + ("filter_mode", "expected_statuses", "expected_torrents"), [ - ("all", ["seeding", "downloading", "stopped"]), - ("started", ["downloading"]), - ("completed", ["seeding"]), - ("paused", ["stopped"]), - ("active", ["seeding", "downloading"]), + ("all", ["seeding", "downloading", "stopped"], [1, 2, 3]), + ("started", ["downloading"], [1]), + ("completed", ["seeding"], [2]), + ("paused", ["stopped"], [3]), + ("active", ["seeding", "downloading"], [1, 2]), ], ) async def test_get_torrents_service( @@ -274,6 +274,7 @@ async def test_get_torrents_service( mock_torrent, filter_mode: str, expected_statuses: list[str], + expected_torrents: list[int], ) -> None: """Test get torrents service with various filter modes.""" client = mock_transmission_client.return_value @@ -316,3 +317,7 @@ async def test_get_torrents_service( assert "name" in torrent_data assert "status" in torrent_data assert torrent_data["name"] == torrent_name + assert torrent_data["id"] in expected_torrents + expected_torrents.remove(int(torrent_data["id"])) + + assert len(expected_torrents) == 0