124 files
This commit is contained in:
@@ -78,9 +78,22 @@ from .const import (
|
||||
CONF_NEXTCLOUD_FOLDER,
|
||||
CONF_NEXTCLOUD_RECURSIVE,
|
||||
CONF_NEXTCLOUD_IMAGE_SIZE,
|
||||
CONF_NEXTCLOUD_SHARE_TOKEN,
|
||||
CONF_NEXTCLOUD_AUTH_MODE,
|
||||
NEXTCLOUD_AUTH_MODE_FOLDER,
|
||||
NEXTCLOUD_AUTH_MODE_PUBLIC,
|
||||
DEFAULT_NEXTCLOUD_AUTH_MODE,
|
||||
DEFAULT_NEXTCLOUD_IMAGE_SIZE,
|
||||
NEXTCLOUD_IMAGE_PREVIEW,
|
||||
NEXTCLOUD_IMAGE_ORIGINAL,
|
||||
CONF_ENTE_URL,
|
||||
CONF_ENTE_ACCESS_TOKEN,
|
||||
CONF_ENTE_COLLECTION_KEY,
|
||||
CONF_ENTE_API_ORIGIN,
|
||||
CONF_ENTE_IMAGE_SIZE,
|
||||
DEFAULT_ENTE_IMAGE_SIZE,
|
||||
ENTE_IMAGE_FULL,
|
||||
ENTE_IMAGE_PREVIEW,
|
||||
DEFAULT_REVERSE_GEOCODE,
|
||||
PROVIDER_GOOGLE_SHARED,
|
||||
PROVIDER_LOCAL_FOLDER,
|
||||
@@ -90,6 +103,7 @@ from .const import (
|
||||
PROVIDER_ICLOUD,
|
||||
PROVIDER_SYNOLOGY,
|
||||
PROVIDER_NEXTCLOUD,
|
||||
PROVIDER_ENTE,
|
||||
DEFAULT_RECURSIVE,
|
||||
)
|
||||
|
||||
@@ -181,6 +195,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if config_entry.data.get(CONF_PROVIDER) in (
|
||||
PROVIDER_LOCAL_FOLDER,
|
||||
PROVIDER_NEXTCLOUD,
|
||||
PROVIDER_ENTE,
|
||||
):
|
||||
return LocalFolderOptionsFlow()
|
||||
return _NoOptionsFlow()
|
||||
@@ -202,6 +217,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_synology()
|
||||
if self._provider == PROVIDER_NEXTCLOUD:
|
||||
return await self.async_step_nextcloud()
|
||||
if self._provider == PROVIDER_ENTE:
|
||||
return await self.async_step_ente()
|
||||
return await self.async_step_google_shared()
|
||||
|
||||
schema = vol.Schema(
|
||||
@@ -213,7 +230,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)",
|
||||
PROVIDER_ICLOUD: "iCloud Shared Album",
|
||||
PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)",
|
||||
PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder, full metadata)",
|
||||
PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder or public album link)",
|
||||
PROVIDER_ENTE: "Ente Photos (public album link)",
|
||||
PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)",
|
||||
})
|
||||
}
|
||||
@@ -976,6 +994,29 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_nextcloud(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Pick between an authenticated WebDAV folder and a public album link."""
|
||||
if user_input is not None:
|
||||
if user_input[CONF_NEXTCLOUD_AUTH_MODE] == NEXTCLOUD_AUTH_MODE_PUBLIC:
|
||||
return await self.async_step_nextcloud_public()
|
||||
return await self.async_step_nextcloud_folder()
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_NEXTCLOUD_AUTH_MODE, default=DEFAULT_NEXTCLOUD_AUTH_MODE
|
||||
): vol.In(
|
||||
{
|
||||
NEXTCLOUD_AUTH_MODE_FOLDER: "Authenticated WebDAV folder",
|
||||
NEXTCLOUD_AUTH_MODE_PUBLIC: "Public album link",
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="nextcloud", data_schema=schema)
|
||||
|
||||
async def async_step_nextcloud_folder(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Collect and validate a Nextcloud WebDAV folder + app password."""
|
||||
errors: dict[str, str] = {}
|
||||
@@ -1010,6 +1051,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
title=name,
|
||||
data={
|
||||
CONF_PROVIDER: PROVIDER_NEXTCLOUD,
|
||||
CONF_NEXTCLOUD_AUTH_MODE: NEXTCLOUD_AUTH_MODE_FOLDER,
|
||||
CONF_NEXTCLOUD_URL: client.base_url,
|
||||
CONF_NEXTCLOUD_USERNAME: username,
|
||||
CONF_NEXTCLOUD_PASSWORD: password,
|
||||
@@ -1043,7 +1085,138 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="nextcloud", data_schema=schema, errors=errors
|
||||
step_id="nextcloud_folder", data_schema=schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_nextcloud_public(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Collect and validate a Nextcloud Photos public album link."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
name = user_input[CONF_ALBUM_NAME].strip()
|
||||
raw_url = user_input[CONF_NEXTCLOUD_URL].strip()
|
||||
size = user_input.get(
|
||||
CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE
|
||||
)
|
||||
|
||||
from . import nextcloud as nc_api
|
||||
|
||||
parsed = nc_api.parse_share_link(raw_url)
|
||||
if parsed is None:
|
||||
errors[CONF_NEXTCLOUD_URL] = "invalid_nextcloud_url"
|
||||
else:
|
||||
base_url, token = parsed
|
||||
client = nc_api.NextcloudPublicClient(self.hass, base_url, token)
|
||||
try:
|
||||
await client.async_validate()
|
||||
except Exception: # noqa: BLE001 - any failure means bad/expired link
|
||||
errors["base"] = "nextcloud_public_cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
f"{DOMAIN}:{PROVIDER_NEXTCLOUD}:{base_url}:{token}:{name}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
CONF_PROVIDER: PROVIDER_NEXTCLOUD,
|
||||
CONF_NEXTCLOUD_AUTH_MODE: NEXTCLOUD_AUTH_MODE_PUBLIC,
|
||||
CONF_NEXTCLOUD_URL: base_url,
|
||||
CONF_NEXTCLOUD_SHARE_TOKEN: token,
|
||||
CONF_NEXTCLOUD_IMAGE_SIZE: size,
|
||||
CONF_ALBUM_NAME: name,
|
||||
},
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ALBUM_NAME): str,
|
||||
vol.Required(CONF_NEXTCLOUD_URL): str,
|
||||
vol.Optional(
|
||||
CONF_NEXTCLOUD_IMAGE_SIZE, default=DEFAULT_NEXTCLOUD_IMAGE_SIZE
|
||||
): vol.In(
|
||||
{
|
||||
NEXTCLOUD_IMAGE_PREVIEW: "Preview (smoothest slideshow)",
|
||||
NEXTCLOUD_IMAGE_ORIGINAL: "Original (full quality, slower)",
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="nextcloud_public", data_schema=schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_ente(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Collect and validate an Ente Photos public album link."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
name = user_input[CONF_ALBUM_NAME].strip()
|
||||
raw_url = user_input[CONF_ENTE_URL].strip()
|
||||
api_origin = (user_input.get(CONF_ENTE_API_ORIGIN) or "").strip()
|
||||
size = user_input.get(CONF_ENTE_IMAGE_SIZE, DEFAULT_ENTE_IMAGE_SIZE)
|
||||
|
||||
from . import ente as ente_api
|
||||
|
||||
share = ente_api.parse_share_link(raw_url)
|
||||
if share is None:
|
||||
errors[CONF_ENTE_URL] = "invalid_ente_url"
|
||||
else:
|
||||
client = ente_api.EnteClient(self.hass, share, api_origin)
|
||||
try:
|
||||
await client.async_validate()
|
||||
except Exception as err: # noqa: BLE001 - bad, expired or private link
|
||||
_LOGGER.warning(
|
||||
"Ente validation failed for %s: %s",
|
||||
share.album_origin,
|
||||
_describe_error(err),
|
||||
)
|
||||
errors["base"] = "ente_cannot_connect"
|
||||
else:
|
||||
from base64 import b64encode
|
||||
|
||||
await self.async_set_unique_id(
|
||||
f"{DOMAIN}:{PROVIDER_ENTE}:{share.access_token}:{name}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
CONF_PROVIDER: PROVIDER_ENTE,
|
||||
CONF_ENTE_URL: share.album_origin,
|
||||
CONF_ENTE_ACCESS_TOKEN: share.access_token,
|
||||
CONF_ENTE_COLLECTION_KEY: b64encode(
|
||||
share.collection_key
|
||||
).decode("ascii"),
|
||||
CONF_ENTE_API_ORIGIN: ente_api.normalize_api_origin(
|
||||
api_origin
|
||||
),
|
||||
CONF_ENTE_IMAGE_SIZE: size,
|
||||
CONF_ALBUM_NAME: name,
|
||||
},
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ALBUM_NAME): str,
|
||||
vol.Required(CONF_ENTE_URL): str,
|
||||
vol.Optional(
|
||||
CONF_ENTE_IMAGE_SIZE, default=DEFAULT_ENTE_IMAGE_SIZE
|
||||
): vol.In(
|
||||
{
|
||||
ENTE_IMAGE_FULL: "Full quality (original file)",
|
||||
ENTE_IMAGE_PREVIEW: "Preview (faster, softer)",
|
||||
}
|
||||
),
|
||||
vol.Optional(CONF_ENTE_API_ORIGIN): str,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="ente", data_schema=schema, errors=errors
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user