Add ability to subscribe to own YouTube channels (#141693)
parent
42d6bd3839
commit
fcd4d3e2df
|
@ -7,7 +7,6 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from youtubeaio.helper import first
|
||||
from youtubeaio.types import AuthScope, ForbiddenError
|
||||
from youtubeaio.youtube import YouTube
|
||||
|
||||
|
@ -96,8 +95,12 @@ class OAuth2FlowHandler(
|
|||
"""Create an entry for the flow, or update existing entry."""
|
||||
try:
|
||||
youtube = await self.get_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||
own_channel = await first(youtube.get_user_channels())
|
||||
if own_channel is None or own_channel.snippet is None:
|
||||
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},
|
||||
|
@ -111,10 +114,10 @@ class OAuth2FlowHandler(
|
|||
except Exception as ex: # noqa: BLE001
|
||||
LOGGER.error("Unknown error occurred: %s", ex.args)
|
||||
return self.async_abort(reason="unknown")
|
||||
self._title = own_channel.snippet.title
|
||||
self._title = own_channels[0].snippet.title
|
||||
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:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
|
@ -138,13 +141,39 @@ class OAuth2FlowHandler(
|
|||
options=user_input,
|
||||
)
|
||||
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 = [
|
||||
SelectOptionDict(
|
||||
value=subscription.snippet.channel_id,
|
||||
label=subscription.snippet.title,
|
||||
value=channel.channel_id,
|
||||
label=f"{channel.snippet.title} (Your Channel)",
|
||||
)
|
||||
async for subscription in youtube.get_user_subscriptions()
|
||||
for channel in own_channels
|
||||
]
|
||||
|
||||
# Add subscribed channels
|
||||
selectable_channels.extend(
|
||||
[
|
||||
SelectOptionDict(
|
||||
value=subscription.snippet.channel_id,
|
||||
label=subscription.snippet.title,
|
||||
)
|
||||
async for subscription in youtube.get_user_subscriptions()
|
||||
]
|
||||
)
|
||||
|
||||
if not selectable_channels:
|
||||
return self.async_abort(reason="no_subscriptions")
|
||||
return self.async_show_form(
|
||||
|
@ -175,13 +204,39 @@ class YouTubeOptionsFlowHandler(OptionsFlow):
|
|||
await youtube.set_user_authentication(
|
||||
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 = [
|
||||
SelectOptionDict(
|
||||
value=subscription.snippet.channel_id,
|
||||
label=subscription.snippet.title,
|
||||
value=channel.channel_id,
|
||||
label=f"{channel.snippet.title} (Your Channel)",
|
||||
)
|
||||
async for subscription in youtube.get_user_subscriptions()
|
||||
for channel in own_channels
|
||||
]
|
||||
|
||||
# Add subscribed channels
|
||||
selectable_channels.extend(
|
||||
[
|
||||
SelectOptionDict(
|
||||
value=subscription.snippet.channel_id,
|
||||
label=subscription.snippet.title,
|
||||
)
|
||||
async for subscription in youtube.get_user_subscriptions()
|
||||
]
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
|
|
|
@ -131,7 +131,51 @@ async def test_flow_abort_without_subscriptions(
|
|||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> 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(
|
||||
"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"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_subscriptions"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
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")
|
||||
|
@ -373,3 +439,112 @@ async def test_options_flow(
|
|||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
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…
Reference in New Issue