mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add Overseerr service to get requests (#134229)
* Add service to get requests * Add service to get requests * Add service to get requests * fix * Add tests
This commit is contained in:
parent
7f473b8260
commit
aab676a313
@ -16,13 +16,24 @@ from homeassistant.components.webhook import (
|
||||
)
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.http import HomeAssistantView
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, JSON_PAYLOAD, LOGGER, REGISTERED_NOTIFICATIONS
|
||||
from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
|
||||
from .services import setup_services
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Overseerr component."""
|
||||
setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool:
|
||||
"""Set up Overseerr from a config entry."""
|
||||
|
@ -9,6 +9,11 @@ LOGGER = logging.getLogger(__package__)
|
||||
|
||||
REQUESTS = "requests"
|
||||
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
ATTR_STATUS = "status"
|
||||
ATTR_SORT_ORDER = "sort_order"
|
||||
ATTR_REQUESTED_BY = "requested_by"
|
||||
|
||||
REGISTERED_NOTIFICATIONS = (
|
||||
NotificationType.REQUEST_PENDING_APPROVAL
|
||||
| NotificationType.REQUEST_APPROVED
|
||||
|
@ -23,5 +23,10 @@
|
||||
"default": "mdi:message-bulleted"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_requests": {
|
||||
"service": "mdi:multimedia"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,13 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
@ -29,10 +23,7 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions or actionable entities.
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
|
115
homeassistant/components/overseerr/services.py
Normal file
115
homeassistant/components/overseerr/services.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""Define services for the Overseerr integration."""
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any, cast
|
||||
|
||||
from python_overseerr import OverseerrClient, OverseerrConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.util.json import JsonValueType
|
||||
|
||||
from .const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_REQUESTED_BY,
|
||||
ATTR_SORT_ORDER,
|
||||
ATTR_STATUS,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import OverseerrConfigEntry
|
||||
|
||||
SERVICE_GET_REQUESTS = "get_requests"
|
||||
SERVICE_GET_REQUESTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
|
||||
vol.Optional(ATTR_STATUS): vol.In(
|
||||
["approved", "pending", "available", "processing", "unavailable", "failed"]
|
||||
),
|
||||
vol.Optional(ATTR_SORT_ORDER): vol.In(["added", "modified"]),
|
||||
vol.Optional(ATTR_REQUESTED_BY): int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> OverseerrConfigEntry:
|
||||
"""Get the Overseerr config entry."""
|
||||
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": DOMAIN},
|
||||
)
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": entry.title},
|
||||
)
|
||||
return cast(OverseerrConfigEntry, entry)
|
||||
|
||||
|
||||
async def get_media(
|
||||
client: OverseerrClient, media_type: str, identifier: int
|
||||
) -> dict[str, Any]:
|
||||
"""Get media details."""
|
||||
media = {}
|
||||
try:
|
||||
if media_type == "movie":
|
||||
media = asdict(await client.get_movie_details(identifier))
|
||||
if media_type == "tv":
|
||||
media = asdict(await client.get_tv_details(identifier))
|
||||
except OverseerrConnectionError:
|
||||
LOGGER.error("Could not find data for %s %s", media_type, identifier)
|
||||
return {}
|
||||
media["media_info"].pop("requests")
|
||||
return media
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Overseerr integration."""
|
||||
|
||||
async def async_get_requests(call: ServiceCall) -> ServiceResponse:
|
||||
"""Get requests made to Overseerr."""
|
||||
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
client = entry.runtime_data.client
|
||||
kwargs: dict[str, Any] = {}
|
||||
if status := call.data.get(ATTR_STATUS):
|
||||
kwargs["status"] = status
|
||||
if sort_order := call.data.get(ATTR_SORT_ORDER):
|
||||
kwargs["sort"] = sort_order
|
||||
if requested_by := call.data.get(ATTR_REQUESTED_BY):
|
||||
kwargs["requested_by"] = requested_by
|
||||
try:
|
||||
requests = await client.get_requests(**kwargs)
|
||||
except OverseerrConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
result: list[dict[str, Any]] = []
|
||||
for request in requests:
|
||||
req = asdict(request)
|
||||
assert request.media.tmdb_id
|
||||
req["media"] = await get_media(
|
||||
client, request.media.media_type, request.media.tmdb_id
|
||||
)
|
||||
result.append(req)
|
||||
|
||||
return {"requests": cast(list[JsonValueType], result)}
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_REQUESTS,
|
||||
async_get_requests,
|
||||
schema=SERVICE_GET_REQUESTS_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
30
homeassistant/components/overseerr/services.yaml
Normal file
30
homeassistant/components/overseerr/services.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
get_requests:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: overseerr
|
||||
status:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- approved
|
||||
- pending
|
||||
- available
|
||||
- processing
|
||||
- unavailable
|
||||
- failed
|
||||
translation_key: request_status
|
||||
sort_order:
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- added
|
||||
- modified
|
||||
translation_key: request_sort_order
|
||||
requested_by:
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
mode: box
|
@ -48,6 +48,54 @@
|
||||
"exceptions": {
|
||||
"connection_error": {
|
||||
"message": "Error connecting to the Overseerr instance: {error}"
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded."
|
||||
},
|
||||
"integration_not_found": {
|
||||
"message": "Integration \"{target}\" not found in registry."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_requests": {
|
||||
"name": "Get requests",
|
||||
"description": "Get media requests from Overseerr.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "Overseerr instance",
|
||||
"description": "The Overseerr instance to get requests from."
|
||||
},
|
||||
"status": {
|
||||
"name": "Request status",
|
||||
"description": "Filter the requests by status."
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "Sort order",
|
||||
"description": "Sort the requests by added or modified date."
|
||||
},
|
||||
"requested_by": {
|
||||
"name": "Requested by",
|
||||
"description": "Filter the requests by the user id that requested them."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"request_status": {
|
||||
"options": {
|
||||
"approved": "Approved",
|
||||
"pending": "Pending",
|
||||
"available": "Available",
|
||||
"processing": "Processing",
|
||||
"unavailable": "Unavailable",
|
||||
"failed": "Failed"
|
||||
}
|
||||
},
|
||||
"request_sort_order": {
|
||||
"options": {
|
||||
"added": "Added",
|
||||
"modified": "Modified"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from python_overseerr import RequestCount
|
||||
from python_overseerr.models import WebhookNotificationConfig
|
||||
from python_overseerr import MovieDetails, RequestCount, RequestResponse
|
||||
from python_overseerr.models import TVDetails, WebhookNotificationConfig
|
||||
|
||||
from homeassistant.components.overseerr.const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
@ -54,6 +54,15 @@ def mock_overseerr_client() -> Generator[AsyncMock]:
|
||||
)
|
||||
)
|
||||
client.test_webhook_notification_config.return_value = True
|
||||
client.get_requests.return_value = RequestResponse.from_json(
|
||||
load_fixture("requests.json", DOMAIN)
|
||||
).results
|
||||
client.get_movie_details.return_value = MovieDetails.from_json(
|
||||
load_fixture("movie.json", DOMAIN)
|
||||
)
|
||||
client.get_tv_details.return_value = TVDetails.from_json(
|
||||
load_fixture("tv.json", DOMAIN)
|
||||
)
|
||||
yield client
|
||||
|
||||
|
||||
|
2644
tests/components/overseerr/fixtures/movie.json
Normal file
2644
tests/components/overseerr/fixtures/movie.json
Normal file
File diff suppressed because it is too large
Load Diff
176
tests/components/overseerr/fixtures/requests.json
Normal file
176
tests/components/overseerr/fixtures/requests.json
Normal file
@ -0,0 +1,176 @@
|
||||
{
|
||||
"pageInfo": {
|
||||
"pages": 2,
|
||||
"pageSize": 10,
|
||||
"results": 14,
|
||||
"page": 1
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": 16,
|
||||
"status": 2,
|
||||
"createdAt": "2024-12-29T10:04:16.000Z",
|
||||
"updatedAt": "2024-12-29T10:04:16.000Z",
|
||||
"type": "movie",
|
||||
"is4k": false,
|
||||
"serverId": 0,
|
||||
"profileId": 7,
|
||||
"rootFolder": "/media/movies",
|
||||
"languageProfileId": null,
|
||||
"tags": [],
|
||||
"isAutoRequest": false,
|
||||
"media": {
|
||||
"downloadStatus": [],
|
||||
"downloadStatus4k": [],
|
||||
"id": 537,
|
||||
"mediaType": "movie",
|
||||
"tmdbId": 1156593,
|
||||
"tvdbId": null,
|
||||
"imdbId": null,
|
||||
"status": 3,
|
||||
"status4k": 1,
|
||||
"createdAt": "2024-12-29T10:04:16.000Z",
|
||||
"updatedAt": "2024-12-29T10:04:17.000Z",
|
||||
"lastSeasonChange": "2024-12-29T10:04:16.000Z",
|
||||
"mediaAddedAt": null,
|
||||
"serviceId": 0,
|
||||
"serviceId4k": null,
|
||||
"externalServiceId": 423,
|
||||
"externalServiceId4k": null,
|
||||
"externalServiceSlug": "1156593",
|
||||
"externalServiceSlug4k": null,
|
||||
"ratingKey": null,
|
||||
"ratingKey4k": null,
|
||||
"serviceUrl": "http://192.168.0.1:7878/movie/1156593"
|
||||
},
|
||||
"seasons": [],
|
||||
"modifiedBy": {
|
||||
"permissions": 2,
|
||||
"id": 1,
|
||||
"email": "one@email.com",
|
||||
"plexUsername": "somebody",
|
||||
"username": null,
|
||||
"recoveryLinkExpirationDate": null,
|
||||
"userType": 1,
|
||||
"plexId": 321321321,
|
||||
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
|
||||
"movieQuotaLimit": null,
|
||||
"movieQuotaDays": null,
|
||||
"tvQuotaLimit": null,
|
||||
"tvQuotaDays": null,
|
||||
"createdAt": "2024-12-16T21:13:58.000Z",
|
||||
"updatedAt": "2024-12-16T23:59:03.000Z",
|
||||
"requestCount": 11,
|
||||
"displayName": "somebody"
|
||||
},
|
||||
"requestedBy": {
|
||||
"permissions": 2,
|
||||
"id": 1,
|
||||
"email": "one@email.com",
|
||||
"plexUsername": "somebody",
|
||||
"username": null,
|
||||
"recoveryLinkExpirationDate": null,
|
||||
"userType": 1,
|
||||
"plexId": 321321321,
|
||||
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
|
||||
"movieQuotaLimit": null,
|
||||
"movieQuotaDays": null,
|
||||
"tvQuotaLimit": null,
|
||||
"tvQuotaDays": null,
|
||||
"createdAt": "2024-12-16T21:13:58.000Z",
|
||||
"updatedAt": "2024-12-16T23:59:03.000Z",
|
||||
"requestCount": 11,
|
||||
"displayName": "somebody"
|
||||
},
|
||||
"seasonCount": 0
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"status": 2,
|
||||
"createdAt": "2024-12-26T14:37:30.000Z",
|
||||
"updatedAt": "2024-12-26T14:37:30.000Z",
|
||||
"type": "tv",
|
||||
"is4k": false,
|
||||
"serverId": 0,
|
||||
"profileId": 7,
|
||||
"rootFolder": "/media/tv",
|
||||
"languageProfileId": 1,
|
||||
"tags": [],
|
||||
"isAutoRequest": false,
|
||||
"media": {
|
||||
"downloadStatus": [],
|
||||
"downloadStatus4k": [],
|
||||
"id": 535,
|
||||
"mediaType": "tv",
|
||||
"tmdbId": 249522,
|
||||
"tvdbId": 447806,
|
||||
"imdbId": null,
|
||||
"status": 4,
|
||||
"status4k": 1,
|
||||
"createdAt": "2024-12-26T14:37:30.000Z",
|
||||
"updatedAt": "2024-12-26T14:45:00.000Z",
|
||||
"lastSeasonChange": "2024-12-26T14:37:30.000Z",
|
||||
"mediaAddedAt": "2024-12-26T14:39:56.000Z",
|
||||
"serviceId": 0,
|
||||
"serviceId4k": null,
|
||||
"externalServiceId": 144,
|
||||
"externalServiceId4k": null,
|
||||
"externalServiceSlug": "beast-games",
|
||||
"externalServiceSlug4k": null,
|
||||
"ratingKey": "10189",
|
||||
"ratingKey4k": null,
|
||||
"plexUrl": "https://app.plex.tv/desktop#!/server/aaaa/details?key=%2Flibrary%2Fmetadata%2F10189",
|
||||
"iOSPlexUrl": "plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F10189&server=aaaa",
|
||||
"serviceUrl": "http://192.168.0.2:8989/series/beast-games"
|
||||
},
|
||||
"seasons": [
|
||||
{
|
||||
"id": 4,
|
||||
"seasonNumber": 1,
|
||||
"status": 2,
|
||||
"createdAt": "2024-12-26T14:37:30.000Z",
|
||||
"updatedAt": "2024-12-26T14:37:30.000Z"
|
||||
}
|
||||
],
|
||||
"modifiedBy": {
|
||||
"permissions": 2,
|
||||
"id": 1,
|
||||
"email": "one@email.com",
|
||||
"plexUsername": "somebody",
|
||||
"username": null,
|
||||
"recoveryLinkExpirationDate": null,
|
||||
"userType": 1,
|
||||
"plexId": 321321321,
|
||||
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
|
||||
"movieQuotaLimit": null,
|
||||
"movieQuotaDays": null,
|
||||
"tvQuotaLimit": null,
|
||||
"tvQuotaDays": null,
|
||||
"createdAt": "2024-12-16T21:13:58.000Z",
|
||||
"updatedAt": "2024-12-16T23:59:03.000Z",
|
||||
"requestCount": 11,
|
||||
"displayName": "somebody"
|
||||
},
|
||||
"requestedBy": {
|
||||
"permissions": 2,
|
||||
"id": 1,
|
||||
"email": "one@email.com",
|
||||
"plexUsername": "somebody",
|
||||
"username": null,
|
||||
"recoveryLinkExpirationDate": null,
|
||||
"userType": 1,
|
||||
"plexId": 321321321,
|
||||
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
|
||||
"movieQuotaLimit": null,
|
||||
"movieQuotaDays": null,
|
||||
"tvQuotaLimit": null,
|
||||
"tvQuotaDays": null,
|
||||
"createdAt": "2024-12-16T21:13:58.000Z",
|
||||
"updatedAt": "2024-12-16T23:59:03.000Z",
|
||||
"requestCount": 11,
|
||||
"displayName": "somebody"
|
||||
},
|
||||
"seasonCount": 1
|
||||
}
|
||||
]
|
||||
}
|
2052
tests/components/overseerr/fixtures/tv.json
Normal file
2052
tests/components/overseerr/fixtures/tv.json
Normal file
File diff suppressed because it is too large
Load Diff
204
tests/components/overseerr/snapshots/test_services.ambr
Normal file
204
tests/components/overseerr/snapshots/test_services.ambr
Normal file
@ -0,0 +1,204 @@
|
||||
# serializer version: 1
|
||||
# name: test_service_get_requests
|
||||
dict({
|
||||
'requests': list([
|
||||
dict({
|
||||
'created_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc),
|
||||
'id': 16,
|
||||
'is4k': False,
|
||||
'media': dict({
|
||||
'adult': False,
|
||||
'budget': 0,
|
||||
'genres': list([
|
||||
dict({
|
||||
'id': 10749,
|
||||
'name': 'Romance',
|
||||
}),
|
||||
dict({
|
||||
'id': 18,
|
||||
'name': 'Drama',
|
||||
}),
|
||||
]),
|
||||
'id': 1156593,
|
||||
'imdb_id': 'tt28510079',
|
||||
'keywords': list([
|
||||
dict({
|
||||
'id': 818,
|
||||
'name': 'based on novel or book',
|
||||
}),
|
||||
dict({
|
||||
'id': 9663,
|
||||
'name': 'sequel',
|
||||
}),
|
||||
]),
|
||||
'media_info': dict({
|
||||
'created_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc),
|
||||
'id': 537,
|
||||
'imdb_id': None,
|
||||
'media_type': <MediaType.MOVIE: 'movie'>,
|
||||
'status': <MediaStatus.PROCESSING: 3>,
|
||||
'tmdb_id': 1156593,
|
||||
'tvdb_id': None,
|
||||
'updated_at': datetime.datetime(2024, 12, 29, 10, 4, 17, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
'original_language': 'es',
|
||||
'original_title': 'Culpa tuya',
|
||||
'overview': "The love between Noah and Nick seems unwavering despite their parents' attempts to separate them. But his job and her entry into college open up their lives to new relationships that will shake the foundations of both their relationship and the Leister family itself.",
|
||||
'popularity': 3958.479,
|
||||
'release_date': datetime.date(2024, 12, 26),
|
||||
'revenue': 0,
|
||||
'runtime': 120,
|
||||
'tagline': 'Divided by family. Driven by love.',
|
||||
'title': 'Your Fault',
|
||||
'vote_average': 7.7,
|
||||
'vote_count': 190,
|
||||
}),
|
||||
'modified_by': dict({
|
||||
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
|
||||
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
|
||||
'display_name': 'somebody',
|
||||
'email': 'one@email.com',
|
||||
'id': 1,
|
||||
'movie_quota_days': None,
|
||||
'movie_quota_limit': None,
|
||||
'plex_id': 321321321,
|
||||
'plex_username': 'somebody',
|
||||
'request_count': 11,
|
||||
'tv_quota_days': None,
|
||||
'tv_quota_limit': None,
|
||||
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
'requested_by': dict({
|
||||
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
|
||||
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
|
||||
'display_name': 'somebody',
|
||||
'email': 'one@email.com',
|
||||
'id': 1,
|
||||
'movie_quota_days': None,
|
||||
'movie_quota_limit': None,
|
||||
'plex_id': 321321321,
|
||||
'plex_username': 'somebody',
|
||||
'request_count': 11,
|
||||
'tv_quota_days': None,
|
||||
'tv_quota_limit': None,
|
||||
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
'season_count': 0,
|
||||
'status': <RequestStatus.APPROVED: 2>,
|
||||
'updated_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
dict({
|
||||
'created_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc),
|
||||
'id': 14,
|
||||
'is4k': False,
|
||||
'media': dict({
|
||||
'first_air_date': datetime.date(2024, 12, 19),
|
||||
'genres': list([
|
||||
dict({
|
||||
'id': 10764,
|
||||
'name': 'Reality',
|
||||
}),
|
||||
]),
|
||||
'id': 249522,
|
||||
'keywords': list([
|
||||
dict({
|
||||
'id': 271,
|
||||
'name': 'competition',
|
||||
}),
|
||||
dict({
|
||||
'id': 4325,
|
||||
'name': 'game show',
|
||||
}),
|
||||
dict({
|
||||
'id': 330122,
|
||||
'name': 'mrbeast',
|
||||
}),
|
||||
]),
|
||||
'languages': list([
|
||||
'da',
|
||||
'en',
|
||||
]),
|
||||
'last_air_date': datetime.date(2024, 12, 26),
|
||||
'last_episode_to_air': dict({
|
||||
'air_date': datetime.date(2024, 12, 26),
|
||||
'episode_number': 3,
|
||||
'id': 5802152,
|
||||
'name': 'The Solitary Experiment',
|
||||
'overview': 'What would happen if three best friends were trapped in a room, but only two could escape? Watch and see for yourself right now!',
|
||||
'still_path': '/r6LRRaA2l2tMDttWbYl3dXdJUij.jpg',
|
||||
}),
|
||||
'media_info': dict({
|
||||
'created_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc),
|
||||
'id': 535,
|
||||
'imdb_id': None,
|
||||
'media_type': <MediaType.TV: 'tv'>,
|
||||
'status': <MediaStatus.PARTIALLY_AVAILABLE: 4>,
|
||||
'tmdb_id': 249522,
|
||||
'tvdb_id': 447806,
|
||||
'updated_at': datetime.datetime(2024, 12, 26, 14, 45, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
'name': 'Beast Games',
|
||||
'next_episode_to_air': dict({
|
||||
'air_date': datetime.date(2025, 1, 2),
|
||||
'episode_number': 4,
|
||||
'id': 5802153,
|
||||
'name': 'Episode 4',
|
||||
'overview': '',
|
||||
'still_path': 'None',
|
||||
}),
|
||||
'number_of_episodes': 10,
|
||||
'number_of_seasons': 1,
|
||||
'original_language': 'en',
|
||||
'original_name': 'Beast Games',
|
||||
'overview': "I gathered 1,000 people to fight for $5,000,000, the LARGEST cash prize in TV history! We're also giving away a private island, Lamborghinis, and millions more in cash throughout the competition! Go watch to see the greatest show ever made!",
|
||||
'popularity': 769.189,
|
||||
'seasons': list([
|
||||
dict({
|
||||
'air_date': datetime.date(2024, 12, 19),
|
||||
'episode_count': 10,
|
||||
'id': 384427,
|
||||
'name': 'Season 1',
|
||||
'overview': '',
|
||||
'poster_path': '/3itZlypnOcVcqI5xxyO6nvJ52yM.jpg',
|
||||
'season_number': 1,
|
||||
}),
|
||||
]),
|
||||
'tagline': '1,000 players. 5 million dollars. 1 winner.',
|
||||
}),
|
||||
'modified_by': dict({
|
||||
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
|
||||
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
|
||||
'display_name': 'somebody',
|
||||
'email': 'one@email.com',
|
||||
'id': 1,
|
||||
'movie_quota_days': None,
|
||||
'movie_quota_limit': None,
|
||||
'plex_id': 321321321,
|
||||
'plex_username': 'somebody',
|
||||
'request_count': 11,
|
||||
'tv_quota_days': None,
|
||||
'tv_quota_limit': None,
|
||||
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
'requested_by': dict({
|
||||
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
|
||||
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
|
||||
'display_name': 'somebody',
|
||||
'email': 'one@email.com',
|
||||
'id': 1,
|
||||
'movie_quota_days': None,
|
||||
'movie_quota_limit': None,
|
||||
'plex_id': 321321321,
|
||||
'plex_username': 'somebody',
|
||||
'request_count': 11,
|
||||
'tv_quota_days': None,
|
||||
'tv_quota_limit': None,
|
||||
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
'season_count': 1,
|
||||
'status': <RequestStatus.APPROVED: 2>,
|
||||
'updated_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
156
tests/components/overseerr/test_services.py
Normal file
156
tests/components/overseerr/test_services.py
Normal file
@ -0,0 +1,156 @@
|
||||
"""Tests for the Overseerr services."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from python_overseerr import OverseerrConnectionError
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.overseerr.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_REQUESTED_BY,
|
||||
ATTR_SORT_ORDER,
|
||||
ATTR_STATUS,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.overseerr.services import SERVICE_GET_REQUESTS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_service_get_requests(
|
||||
hass: HomeAssistant,
|
||||
mock_overseerr_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the get_requests service."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_REQUESTS,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_STATUS: "approved",
|
||||
ATTR_SORT_ORDER: "added",
|
||||
ATTR_REQUESTED_BY: 1,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
for request in response["requests"]:
|
||||
assert "requests" not in request["media"]["media_info"]
|
||||
mock_overseerr_client.get_requests.assert_called_once_with(
|
||||
status="approved", sort="added", requested_by=1
|
||||
)
|
||||
|
||||
|
||||
async def test_service_get_requests_no_meta(
|
||||
hass: HomeAssistant,
|
||||
mock_overseerr_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the get_requests service."""
|
||||
mock_overseerr_client.get_movie_details.side_effect = OverseerrConnectionError
|
||||
mock_overseerr_client.get_tv_details.side_effect = OverseerrConnectionError
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_REQUESTS,
|
||||
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
for request in response["requests"]:
|
||||
assert request["media"] == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "payload", "function", "exception", "raised_exception", "message"),
|
||||
[
|
||||
(
|
||||
SERVICE_GET_REQUESTS,
|
||||
{},
|
||||
"get_requests",
|
||||
OverseerrConnectionError("Timeout"),
|
||||
HomeAssistantError,
|
||||
"Error connecting to the Overseerr instance: Timeout",
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_services_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_overseerr_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
payload: dict[str, str],
|
||||
function: str,
|
||||
exception: Exception,
|
||||
raised_exception: type[Exception],
|
||||
message: str,
|
||||
) -> None:
|
||||
"""Test a connection error in the services."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
getattr(mock_overseerr_client, function).side_effect = exception
|
||||
|
||||
with pytest.raises(raised_exception, match=message):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload,
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "payload"),
|
||||
[
|
||||
(SERVICE_GET_REQUESTS, {}),
|
||||
],
|
||||
)
|
||||
async def test_service_entry_availability(
|
||||
hass: HomeAssistant,
|
||||
mock_overseerr_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
payload: dict[str, str],
|
||||
) -> None:
|
||||
"""Test the services without valid entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry2 = MockConfigEntry(domain=DOMAIN)
|
||||
mock_config_entry2.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="Mock Title is not loaded"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
{ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload,
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match='Integration "overseerr" not found in registry'
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
{ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload,
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user