Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
869778e
Initial commit after scaffold setup
RunC0deRun Nov 16, 2020
21d9d3d
Add initial config flow
RunC0deRun Nov 17, 2020
2b1c8c5
Create initial entity
RunC0deRun Nov 21, 2020
6de1430
Ready for testing
RunC0deRun Dec 3, 2020
c51b1e0
Can browse, no result yet
RunC0deRun Dec 4, 2020
65fed4e
Further improvements. Browsing is working.
RunC0deRun Dec 6, 2020
837c2fb
Two valid URLs. Do not play in HA
RunC0deRun Dec 8, 2020
e420dd4
First working version for music
RunC0deRun Dec 12, 2020
25f52ef
Add thumbnail
RunC0deRun Dec 12, 2020
12457f8
Includes Artist->Album hierarchy
RunC0deRun Dec 13, 2020
75f76c5
Add sorting of artists, albums and tracks
RunC0deRun Dec 14, 2020
1afa735
Remove code for video libraries
RunC0deRun Dec 14, 2020
3075004
Improved code styling
RunC0deRun Dec 14, 2020
cbed5cc
Optimize configuration flow
RunC0deRun Dec 15, 2020
a494779
Fix unit tests for config flow
RunC0deRun Dec 16, 2020
d60fd3e
Fix import order
RunC0deRun Dec 16, 2020
9644fd2
Conform to style requirements
RunC0deRun Dec 16, 2020
661be5f
Use empty string as media type for non playables
RunC0deRun Dec 18, 2020
a3fcc05
100% code coverage config_flow
RunC0deRun Dec 19, 2020
5ec0d6d
Type async_get_media_source
RunC0deRun Dec 20, 2020
9b1970f
Final docsctring fix after rebase
RunC0deRun Dec 20, 2020
3e45b28
Add __init__ and media_source files to .coveragerc
RunC0deRun Dec 20, 2020
83435de
Fix testing issues after rebase
RunC0deRun Apr 23, 2021
dd23a39
Fix string format issues and relative const import
RunC0deRun Apr 23, 2021
4a98c21
Remove unused manifest entries
RunC0deRun Apr 24, 2021
5c81506
Raise ConfigEntry exceptions, not log errors
RunC0deRun Apr 26, 2021
1720d29
Upgrade dependency to avoid WARNING on startup
RunC0deRun Apr 26, 2021
cfdf210
Change to builtin tuple and list (deprecation)
RunC0deRun Apr 26, 2021
85592eb
Log broad exceptions
RunC0deRun Apr 26, 2021
b7e25d2
Add strict typing
RunC0deRun Apr 26, 2021
dc05764
Further type fixes after rebase
RunC0deRun Apr 26, 2021
5be9da7
Retry when cannot connect, otherwise fail setup
RunC0deRun Apr 26, 2021
d6211ca
Remove unused CONFIG_SCHEMA
RunC0deRun Apr 27, 2021
8b3dfe8
Enable strict typing checks
RunC0deRun Apr 27, 2021
a0f3bd8
FlowResultDict -> FlowResult
RunC0deRun Apr 29, 2021
f2defe4
Code quality improvements
RunC0deRun Apr 30, 2021
402d484
Resolve mypy.ini merge conflict
RunC0deRun May 3, 2021
8bc5932
Use unique userid generated by Jellyfin
RunC0deRun May 4, 2021
9adb449
Update homeassistant/components/jellyfin/config_flow.py
RunC0deRun May 7, 2021
1a065ff
Minor changes for additional checks after rebase
RunC0deRun Jul 4, 2021
a4bf813
Remove title from string and translations
RunC0deRun Aug 7, 2021
b7b0e14
Changes wrt review
RunC0deRun Aug 25, 2021
8eefa9c
Fixes based on rebase and review suggestions
RunC0deRun Nov 9, 2021
8315631
Move client initialization to separate file
RunC0deRun Nov 9, 2021
1bd5f1d
Remove persistent_notification, add test const.py
RunC0deRun Nov 10, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ omit =
homeassistant/components/isy994/switch.py
homeassistant/components/itach/remote.py
homeassistant/components/itunes/media_player.py
homeassistant/components/jellyfin/__init__.py
homeassistant/components/jellyfin/media_source.py
Comment thread
RunC0deRun marked this conversation as resolved.
homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/__init__.py
homeassistant/components/juicenet/const.py
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ homeassistant.components.image_processing.*
homeassistant.components.input_select.*
homeassistant.components.integration.*
homeassistant.components.iqvia.*
homeassistant.components.jellyfin.*
homeassistant.components.jewish_calendar.*
homeassistant.components.knx.*
homeassistant.components.kraken.*
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/islamic_prayer_times/* @engrbm87
homeassistant/components/isy994/* @bdraco @shbatm
homeassistant/components/izone/* @Swamp-Ig
homeassistant/components/jellyfin/* @j-stienstra
homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/juicenet/* @jesserockz
homeassistant/components/kaiterra/* @Michsior14
Expand Down
36 changes: 36 additions & 0 deletions homeassistant/components/jellyfin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""The Jellyfin integration."""
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
from .const import DATA_CLIENT, DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Jellyfin from a config entry."""
hass.data.setdefault(DOMAIN, {})

client = create_client()
try:
await validate_input(hass, dict(entry.data), client)
except CannotConnect as ex:
raise ConfigEntryNotReady("Cannot connect to Jellyfin server") from ex
except InvalidAuth:
_LOGGER.error("Failed to login to Jellyfin server")
return False
Comment thread
RunC0deRun marked this conversation as resolved.
Outdated
else:
hass.data[DOMAIN][entry.entry_id] = {DATA_CLIENT: client}

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass.data[DOMAIN].pop(entry.entry_id)

return True
94 changes: 94 additions & 0 deletions homeassistant/components/jellyfin/client_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Utility methods for initializing a Jellyfin client."""
from __future__ import annotations

import socket
from typing import Any
import uuid

from jellyfin_apiclient_python import Jellyfin, JellyfinClient
from jellyfin_apiclient_python.api import API
from jellyfin_apiclient_python.connection_manager import (
CONNECTION_STATE,
ConnectionManager,
)

from homeassistant import exceptions
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant

from .const import CLIENT_VERSION, USER_AGENT, USER_APP_NAME


async def validate_input(
hass: HomeAssistant, user_input: dict[str, Any], client: JellyfinClient
) -> str:
"""Validate that the provided url and credentials can be used to connect."""
url = user_input[CONF_URL]
username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD]

userid = await hass.async_add_executor_job(
_connect, client, url, username, password
)

return userid


def create_client() -> JellyfinClient:
"""Create a new Jellyfin client."""
jellyfin = Jellyfin()
client = jellyfin.get_client()
_setup_client(client)
return client


def _setup_client(client: JellyfinClient) -> None:
"""Configure the Jellyfin client with a number of required properties."""
player_name = socket.gethostname()
client_uuid = str(uuid.uuid4())

client.config.app(USER_APP_NAME, CLIENT_VERSION, player_name, client_uuid)
client.config.http(USER_AGENT)


def _connect(client: JellyfinClient, url: str, username: str, password: str) -> str:
"""Connect to the Jellyfin server and assert that the user can login."""
client.config.data["auth.ssl"] = url.startswith("https")

_connect_to_address(client.auth, url)
_login(client.auth, url, username, password)
return _get_id(client.jellyfin)


def _connect_to_address(connection_manager: ConnectionManager, url: str) -> None:
"""Connect to the Jellyfin server."""
state = connection_manager.connect_to_address(url)
if state["State"] != CONNECTION_STATE["ServerSignIn"]:
raise CannotConnect


def _login(
connection_manager: ConnectionManager,
url: str,
username: str,
password: str,
) -> None:
"""Assert that the user can log in to the Jellyfin server."""
response = connection_manager.login(url, username, password)
if "AccessToken" not in response:
raise InvalidAuth


def _get_id(api: API) -> str:
"""Set the unique userid from a Jellyfin server."""
settings: dict[str, Any] = api.get_user_settings()
userid: str = settings["Id"]
return userid


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate the server is unreachable."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate the credentials are invalid."""
62 changes: 62 additions & 0 deletions homeassistant/components/jellyfin/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Config flow for the Jellyfin integration."""
from __future__ import annotations

import logging
from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult

from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Jellyfin."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a user defined configuration."""
if self._async_current_entries():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we only allow a single entry?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is not a technical limitation. I've added the restriction to make the initial submit as limited in scope as possible. I can work on supporting multiple entries in future versions of the integration.

return self.async_abort(reason="single_instance_allowed")

errors: dict[str, str] = {}

if user_input is not None:
client = create_client()
try:
userid = await validate_input(self.hass, user_input, client)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as ex: # pylint: disable=broad-except
errors["base"] = "unknown"
_LOGGER.exception(ex)
else:
await self.async_set_unique_id(userid)
self._abort_if_unique_id_configured()

return self.async_create_entry(
title=user_input[CONF_URL], data=user_input
)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
40 changes: 40 additions & 0 deletions homeassistant/components/jellyfin/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Constants for the Jellyfin integration."""

from typing import Final

DOMAIN: Final = "jellyfin"

CLIENT_VERSION: Final = "1.0"

COLLECTION_TYPE_MOVIES: Final = "movies"
COLLECTION_TYPE_TVSHOWS: Final = "tvshows"
COLLECTION_TYPE_MUSIC: Final = "music"

DATA_CLIENT: Final = "client"

ITEM_KEY_COLLECTION_TYPE: Final = "CollectionType"
ITEM_KEY_ID: Final = "Id"
ITEM_KEY_IMAGE_TAGS: Final = "ImageTags"
ITEM_KEY_INDEX_NUMBER: Final = "IndexNumber"
ITEM_KEY_MEDIA_SOURCES: Final = "MediaSources"
ITEM_KEY_MEDIA_TYPE: Final = "MediaType"
ITEM_KEY_NAME: Final = "Name"

ITEM_TYPE_ALBUM: Final = "MusicAlbum"
ITEM_TYPE_ARTIST: Final = "MusicArtist"
ITEM_TYPE_AUDIO: Final = "Audio"
ITEM_TYPE_LIBRARY: Final = "CollectionFolder"

MAX_IMAGE_WIDTH: Final = 500
MAX_STREAMING_BITRATE: Final = "140000000"


MEDIA_SOURCE_KEY_PATH: Final = "Path"

MEDIA_TYPE_AUDIO: Final = "Audio"
MEDIA_TYPE_NONE: Final = ""

SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC]

USER_APP_NAME: Final = "Home Assistant"
USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}"
13 changes: 13 additions & 0 deletions homeassistant/components/jellyfin/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"domain": "jellyfin",
"name": "Jellyfin",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/jellyfin",
"requirements": [
"jellyfin-apiclient-python==1.7.2"
],
"iot_class": "local_polling",
"codeowners": [
"@j-stienstra"
]
}
Loading