mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add ability to subscribe to own YouTube channels (#141693)
This commit is contained in:
parent
42d6bd3839
commit
fcd4d3e2df
@ -7,7 +7,6 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from youtubeaio.helper import first
|
|
||||||
from youtubeaio.types import AuthScope, ForbiddenError
|
from youtubeaio.types import AuthScope, ForbiddenError
|
||||||
from youtubeaio.youtube import YouTube
|
from youtubeaio.youtube import YouTube
|
||||||
|
|
||||||
@ -96,8 +95,12 @@ class OAuth2FlowHandler(
|
|||||||
"""Create an entry for the flow, or update existing entry."""
|
"""Create an entry for the flow, or update existing entry."""
|
||||||
try:
|
try:
|
||||||
youtube = await self.get_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
youtube = await self.get_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||||
own_channel = await first(youtube.get_user_channels())
|
own_channels = [
|
||||||
if own_channel is None or own_channel.snippet is None:
|
channel
|
||||||
|
async for channel in youtube.get_user_channels()
|
||||||
|
if channel.snippet is not None
|
||||||
|
]
|
||||||
|
if not own_channels:
|
||||||
return self.async_abort(
|
return self.async_abort(
|
||||||
reason="no_channel",
|
reason="no_channel",
|
||||||
description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL},
|
description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL},
|
||||||
@ -111,10 +114,10 @@ class OAuth2FlowHandler(
|
|||||||
except Exception as ex: # noqa: BLE001
|
except Exception as ex: # noqa: BLE001
|
||||||
LOGGER.error("Unknown error occurred: %s", ex.args)
|
LOGGER.error("Unknown error occurred: %s", ex.args)
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
self._title = own_channel.snippet.title
|
self._title = own_channels[0].snippet.title
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
await self.async_set_unique_id(own_channel.channel_id)
|
await self.async_set_unique_id(own_channels[0].channel_id)
|
||||||
if self.source != SOURCE_REAUTH:
|
if self.source != SOURCE_REAUTH:
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
@ -138,13 +141,39 @@ class OAuth2FlowHandler(
|
|||||||
options=user_input,
|
options=user_input,
|
||||||
)
|
)
|
||||||
youtube = await self.get_resource(self._data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
youtube = await self.get_resource(self._data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||||
|
|
||||||
|
# Get user's own channels
|
||||||
|
own_channels = [
|
||||||
|
channel
|
||||||
|
async for channel in youtube.get_user_channels()
|
||||||
|
if channel.snippet is not None
|
||||||
|
]
|
||||||
|
if not own_channels:
|
||||||
|
return self.async_abort(
|
||||||
|
reason="no_channel",
|
||||||
|
description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start with user's own channels
|
||||||
selectable_channels = [
|
selectable_channels = [
|
||||||
|
SelectOptionDict(
|
||||||
|
value=channel.channel_id,
|
||||||
|
label=f"{channel.snippet.title} (Your Channel)",
|
||||||
|
)
|
||||||
|
for channel in own_channels
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add subscribed channels
|
||||||
|
selectable_channels.extend(
|
||||||
|
[
|
||||||
SelectOptionDict(
|
SelectOptionDict(
|
||||||
value=subscription.snippet.channel_id,
|
value=subscription.snippet.channel_id,
|
||||||
label=subscription.snippet.title,
|
label=subscription.snippet.title,
|
||||||
)
|
)
|
||||||
async for subscription in youtube.get_user_subscriptions()
|
async for subscription in youtube.get_user_subscriptions()
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if not selectable_channels:
|
if not selectable_channels:
|
||||||
return self.async_abort(reason="no_subscriptions")
|
return self.async_abort(reason="no_subscriptions")
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@ -175,13 +204,39 @@ class YouTubeOptionsFlowHandler(OptionsFlow):
|
|||||||
await youtube.set_user_authentication(
|
await youtube.set_user_authentication(
|
||||||
self.config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN], [AuthScope.READ_ONLY]
|
self.config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN], [AuthScope.READ_ONLY]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get user's own channels
|
||||||
|
own_channels = [
|
||||||
|
channel
|
||||||
|
async for channel in youtube.get_user_channels()
|
||||||
|
if channel.snippet is not None
|
||||||
|
]
|
||||||
|
if not own_channels:
|
||||||
|
return self.async_abort(
|
||||||
|
reason="no_channel",
|
||||||
|
description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start with user's own channels
|
||||||
selectable_channels = [
|
selectable_channels = [
|
||||||
|
SelectOptionDict(
|
||||||
|
value=channel.channel_id,
|
||||||
|
label=f"{channel.snippet.title} (Your Channel)",
|
||||||
|
)
|
||||||
|
for channel in own_channels
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add subscribed channels
|
||||||
|
selectable_channels.extend(
|
||||||
|
[
|
||||||
SelectOptionDict(
|
SelectOptionDict(
|
||||||
value=subscription.snippet.channel_id,
|
value=subscription.snippet.channel_id,
|
||||||
label=subscription.snippet.title,
|
label=subscription.snippet.title,
|
||||||
)
|
)
|
||||||
async for subscription in youtube.get_user_subscriptions()
|
async for subscription in youtube.get_user_subscriptions()
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=self.add_suggested_values_to_schema(
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
@ -131,7 +131,51 @@ async def test_flow_abort_without_subscriptions(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check abort flow if user has no subscriptions."""
|
"""Check abort flow if user has no subscriptions and no own channel."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
"youtube", context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": "https://example.com/auth/external/callback",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||||
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
|
f"&state={state}&scope={'+'.join(SCOPES)}"
|
||||||
|
"&access_type=offline&prompt=consent"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
service = MockYouTube(
|
||||||
|
channel_fixture="youtube/get_no_channel.json",
|
||||||
|
subscriptions_fixture="youtube/get_no_subscriptions.json",
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.youtube.async_setup_entry", return_value=True),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.youtube.config_flow.YouTube", return_value=service
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "no_channel"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
|
async def test_flow_without_subscriptions(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Check flow continues even without subscriptions since user has their own channel."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
"youtube", context={"source": config_entries.SOURCE_USER}
|
"youtube", context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
@ -163,8 +207,30 @@ async def test_flow_abort_without_subscriptions(
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["reason"] == "no_subscriptions"
|
assert result["step_id"] == "channels"
|
||||||
|
|
||||||
|
# Verify the form schema contains only the user's own channel
|
||||||
|
schema = result["data_schema"]
|
||||||
|
channels = schema.schema[CONF_CHANNELS].config["options"]
|
||||||
|
assert len(channels) == 1
|
||||||
|
assert channels[0]["value"] == "UC_x5XG1OV2P6uZZ5FSM9Ttw"
|
||||||
|
assert "(Your Channel)" in channels[0]["label"]
|
||||||
|
|
||||||
|
# Test selecting the own channel
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == TITLE
|
||||||
|
assert "result" in result
|
||||||
|
assert result["result"].unique_id == "UC_x5XG1OV2P6uZZ5FSM9Ttw"
|
||||||
|
assert "token" in result["result"].data
|
||||||
|
assert result["result"].data["token"]["access_token"] == "mock-access-token"
|
||||||
|
assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token"
|
||||||
|
assert result["options"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("current_request_with_host")
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
@ -373,3 +439,112 @@ async def test_options_flow(
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result["data"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}
|
assert result["data"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
|
async def test_own_channel_included(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the user's own channel is included in the list of selectable channels."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
"youtube", context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": "https://example.com/auth/external/callback",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||||
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
|
f"&state={state}&scope={'+'.join(SCOPES)}"
|
||||||
|
"&access_type=offline&prompt=consent"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.youtube.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.youtube.config_flow.YouTube",
|
||||||
|
return_value=MockYouTube(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "channels"
|
||||||
|
|
||||||
|
# Verify the form schema contains the user's own channel
|
||||||
|
schema = result["data_schema"]
|
||||||
|
channels = schema.schema[CONF_CHANNELS].config["options"]
|
||||||
|
assert any(
|
||||||
|
channel["value"] == "UC_x5XG1OV2P6uZZ5FSM9Ttw"
|
||||||
|
and "(Your Channel)" in channel["label"]
|
||||||
|
for channel in channels
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test selecting both own channel and a subscribed channel
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={
|
||||||
|
CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw", "UC_x5XG1OV2P6uZZ5FSM9Ttw"]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == TITLE
|
||||||
|
assert "result" in result
|
||||||
|
assert result["result"].unique_id == "UC_x5XG1OV2P6uZZ5FSM9Ttw"
|
||||||
|
assert "token" in result["result"].data
|
||||||
|
assert result["result"].data["token"]["access_token"] == "mock-access-token"
|
||||||
|
assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token"
|
||||||
|
assert result["options"] == {
|
||||||
|
CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw", "UC_x5XG1OV2P6uZZ5FSM9Ttw"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow_own_channel(
|
||||||
|
hass: HomeAssistant, setup_integration: ComponentSetup
|
||||||
|
) -> None:
|
||||||
|
"""Test the options flow includes the user's own channel."""
|
||||||
|
await setup_integration()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.youtube.config_flow.YouTube",
|
||||||
|
return_value=MockYouTube(),
|
||||||
|
):
|
||||||
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
# Verify the form schema contains the user's own channel
|
||||||
|
schema = result["data_schema"]
|
||||||
|
channels = schema.schema[CONF_CHANNELS].config["options"]
|
||||||
|
assert any(
|
||||||
|
channel["value"] == "UC_x5XG1OV2P6uZZ5FSM9Ttw"
|
||||||
|
and "(Your Channel)" in channel["label"]
|
||||||
|
for channel in channels
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["data"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user