Skip to content

Commit fe363f3

Browse files
authored
Jellyfin native client controls (home-assistant#161982)
1 parent 31562e7 commit fe363f3

6 files changed

Lines changed: 362 additions & 9 deletions

File tree

homeassistant/components/jellyfin/media_player.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def _update_from_session_data(self) -> None:
150150

151151
self._attr_state = state
152152
self._attr_is_volume_muted = volume_muted
153-
self._attr_volume_level = volume_level
153+
# Only update volume_level if the API provides it, otherwise preserve current value
154+
if volume_level is not None:
155+
self._attr_volume_level = volume_level
154156
self._attr_media_content_type = media_content_type
155157
self._attr_media_content_id = media_content_id
156158
self._attr_media_title = media_title
@@ -190,7 +192,9 @@ def supported_features(self) -> MediaPlayerEntityFeature:
190192
)
191193
features = MediaPlayerEntityFeature(0)
192194

193-
if "PlayMediaSource" in commands:
195+
if "PlayMediaSource" in commands or self.capabilities.get(
196+
"SupportsMediaControl", False
197+
):
194198
features |= (
195199
MediaPlayerEntityFeature.BROWSE_MEDIA
196200
| MediaPlayerEntityFeature.PLAY_MEDIA
@@ -201,10 +205,10 @@ def supported_features(self) -> MediaPlayerEntityFeature:
201205
| MediaPlayerEntityFeature.SEARCH_MEDIA
202206
)
203207

204-
if "Mute" in commands:
208+
if "Mute" in commands and "Unmute" in commands:
205209
features |= MediaPlayerEntityFeature.VOLUME_MUTE
206210

207-
if "VolumeSet" in commands:
211+
if "VolumeSet" in commands or "SetVolume" in commands:
208212
features |= MediaPlayerEntityFeature.VOLUME_SET
209213

210214
return features
@@ -219,11 +223,13 @@ def media_pause(self) -> None:
219223
"""Send pause command."""
220224
self.coordinator.api_client.jellyfin.remote_pause(self.session_id)
221225
self._attr_state = MediaPlayerState.PAUSED
226+
self.schedule_update_ha_state()
222227

223228
def media_play(self) -> None:
224229
"""Send play command."""
225230
self.coordinator.api_client.jellyfin.remote_unpause(self.session_id)
226231
self._attr_state = MediaPlayerState.PLAYING
232+
self.schedule_update_ha_state()
227233

228234
def media_play_pause(self) -> None:
229235
"""Send the PlayPause command to the session."""
@@ -233,6 +239,7 @@ def media_stop(self) -> None:
233239
"""Send stop command."""
234240
self.coordinator.api_client.jellyfin.remote_stop(self.session_id)
235241
self._attr_state = MediaPlayerState.IDLE
242+
self.schedule_update_ha_state()
236243

237244
def play_media(
238245
self, media_type: MediaType | str, media_id: str, **kwargs: Any
@@ -247,13 +254,17 @@ def set_volume_level(self, volume: float) -> None:
247254
self.coordinator.api_client.jellyfin.remote_set_volume(
248255
self.session_id, int(volume * 100)
249256
)
257+
self._attr_volume_level = volume
258+
self.schedule_update_ha_state()
250259

251260
def mute_volume(self, mute: bool) -> None:
252261
"""Mute the volume."""
253262
if mute:
254263
self.coordinator.api_client.jellyfin.remote_mute(self.session_id)
255264
else:
256265
self.coordinator.api_client.jellyfin.remote_unmute(self.session_id)
266+
self._attr_is_volume_muted = mute
267+
self.schedule_update_ha_state()
257268

258269
async def async_browse_media(
259270
self,

tests/components/jellyfin/fixtures/sessions-new-client.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
],
2222
"Capabilities": {
2323
"PlayableMediaTypes": ["Video"],
24-
"SupportedCommands": ["VolumeSet", "Mute"],
24+
"SupportedCommands": ["VolumeSet", "Mute", "Unmute"],
2525
"SupportsMediaControl": true,
2626
"SupportsContentUploading": true,
2727
"MessageCallbackUrl": "string",
@@ -1781,7 +1781,7 @@
17811781
],
17821782
"Capabilities": {
17831783
"PlayableMediaTypes": ["Video"],
1784-
"SupportedCommands": ["VolumeSet", "Mute"],
1784+
"SupportedCommands": ["VolumeSet", "Mute", "Unmute"],
17851785
"SupportsMediaControl": true,
17861786
"SupportsContentUploading": true,
17871787
"MessageCallbackUrl": "string",

tests/components/jellyfin/fixtures/sessions.json

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
],
2222
"Capabilities": {
2323
"PlayableMediaTypes": ["Video"],
24-
"SupportedCommands": ["VolumeSet", "Mute", "PlayMediaSource"],
24+
"SupportedCommands": ["VolumeSet", "Mute", "Unmute", "PlayMediaSource"],
2525
"SupportsMediaControl": true,
2626
"SupportsContentUploading": true,
2727
"MessageCallbackUrl": "string",
@@ -1781,7 +1781,7 @@
17811781
],
17821782
"Capabilities": {
17831783
"PlayableMediaTypes": ["Video"],
1784-
"SupportedCommands": ["VolumeSet", "Mute"],
1784+
"SupportedCommands": ["VolumeSet", "Mute", "Unmute"],
17851785
"SupportsMediaControl": true,
17861786
"SupportsContentUploading": true,
17871787
"MessageCallbackUrl": "string",
@@ -4548,5 +4548,133 @@
45484548
"PlayMediaSource",
45494549
"PlayTrailers"
45504550
]
4551+
},
4552+
{
4553+
"PlayState": {
4554+
"PositionTicks": 100000000,
4555+
"CanSeek": true,
4556+
"IsPaused": false,
4557+
"IsMuted": false,
4558+
"VolumeLevel": 50,
4559+
"AudioStreamIndex": 0,
4560+
"SubtitleStreamIndex": 0,
4561+
"MediaSourceId": "string",
4562+
"PlayMethod": "Transcode",
4563+
"RepeatMode": "RepeatNone",
4564+
"LiveStreamId": "string"
4565+
},
4566+
"AdditionalUsers": [],
4567+
"Capabilities": {
4568+
"PlayableMediaTypes": ["Video"],
4569+
"SupportedCommands": ["Mute", "VolumeSet", "PlayMediaSource"],
4570+
"SupportsMediaControl": true,
4571+
"SupportsContentUploading": false,
4572+
"MessageCallbackUrl": "string",
4573+
"SupportsPersistentIdentifier": false,
4574+
"SupportsSync": false,
4575+
"DeviceProfile": null
4576+
},
4577+
"RemoteEndPoint": "192.168.1.1",
4578+
"PlayableMediaTypes": ["Video"],
4579+
"Id": "SESSION-UUID-FIVE",
4580+
"UserId": "USER-UUID",
4581+
"UserName": "USER",
4582+
"Client": "Test Client Five",
4583+
"LastActivityDate": "2021-05-21T06:09:06.919Z",
4584+
"LastPlaybackCheckIn": "2021-05-21T06:09:06.919Z",
4585+
"DeviceName": "JELLYFIN-DEVICE-FIVE",
4586+
"DeviceId": "DEVICE-UUID-FIVE",
4587+
"ApplicationVersion": "1.0.0",
4588+
"IsActive": true,
4589+
"SupportsMediaControl": true,
4590+
"SupportsRemoteControl": true,
4591+
"HasCustomDeviceName": false,
4592+
"ServerId": "SERVER-UUID",
4593+
"SupportedCommands": ["MoveUp"],
4594+
"NowPlayingItem": {
4595+
"Name": "TEST VIDEO",
4596+
"ServerId": "SERVER-UUID",
4597+
"Id": "VIDEO-UUID-FIVE",
4598+
"RunTimeTicks": 600000000,
4599+
"Type": "Episode",
4600+
"UserData": {
4601+
"PlaybackPositionTicks": 0,
4602+
"PlayCount": 0,
4603+
"IsFavorite": false,
4604+
"Played": false,
4605+
"Key": "string"
4606+
},
4607+
"PrimaryImageAspectRatio": 1,
4608+
"SeriesName": "TEST SERIES",
4609+
"ParentIndexNumber": 1,
4610+
"IndexNumber": 1,
4611+
"ImageTags": {
4612+
"Primary": "tag"
4613+
}
4614+
}
4615+
},
4616+
{
4617+
"PlayState": {
4618+
"PositionTicks": 100000000,
4619+
"CanSeek": true,
4620+
"IsPaused": false,
4621+
"IsMuted": false,
4622+
"VolumeLevel": 50,
4623+
"AudioStreamIndex": 0,
4624+
"SubtitleStreamIndex": 0,
4625+
"MediaSourceId": "string",
4626+
"PlayMethod": "Transcode",
4627+
"RepeatMode": "RepeatNone",
4628+
"LiveStreamId": "string"
4629+
},
4630+
"AdditionalUsers": [],
4631+
"Capabilities": {
4632+
"PlayableMediaTypes": ["Video"],
4633+
"SupportedCommands": ["Unmute", "VolumeSet", "PlayMediaSource"],
4634+
"SupportsMediaControl": true,
4635+
"SupportsContentUploading": false,
4636+
"MessageCallbackUrl": "string",
4637+
"SupportsPersistentIdentifier": false,
4638+
"SupportsSync": false,
4639+
"DeviceProfile": null
4640+
},
4641+
"RemoteEndPoint": "192.168.1.1",
4642+
"PlayableMediaTypes": ["Video"],
4643+
"Id": "SESSION-UUID-SIX",
4644+
"UserId": "USER-UUID",
4645+
"UserName": "USER",
4646+
"Client": "Test Client Six",
4647+
"LastActivityDate": "2021-05-21T06:09:06.919Z",
4648+
"LastPlaybackCheckIn": "2021-05-21T06:09:06.919Z",
4649+
"DeviceName": "JELLYFIN-DEVICE-SIX",
4650+
"DeviceId": "DEVICE-UUID-SIX",
4651+
"ApplicationVersion": "1.0.0",
4652+
"IsActive": true,
4653+
"SupportsMediaControl": true,
4654+
"SupportsRemoteControl": true,
4655+
"HasCustomDeviceName": false,
4656+
"ServerId": "SERVER-UUID",
4657+
"SupportedCommands": ["MoveUp"],
4658+
"NowPlayingItem": {
4659+
"Name": "TEST VIDEO",
4660+
"ServerId": "SERVER-UUID",
4661+
"Id": "VIDEO-UUID-SIX",
4662+
"RunTimeTicks": 600000000,
4663+
"Type": "Episode",
4664+
"UserData": {
4665+
"PlaybackPositionTicks": 0,
4666+
"PlayCount": 0,
4667+
"IsFavorite": false,
4668+
"Played": false,
4669+
"Key": "string"
4670+
},
4671+
"PrimaryImageAspectRatio": 1,
4672+
"SeriesName": "TEST SERIES",
4673+
"ParentIndexNumber": 1,
4674+
"IndexNumber": 1,
4675+
"ImageTags": {
4676+
"Primary": "tag"
4677+
}
4678+
}
45514679
}
45524680
]

tests/components/jellyfin/snapshots/test_diagnostics.ambr

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
'SupportedCommands': list([
183183
'VolumeSet',
184184
'Mute',
185+
'Unmute',
185186
'PlayMediaSource',
186187
]),
187188
'SupportsContentUploading': True,
@@ -902,6 +903,7 @@
902903
'SupportedCommands': list([
903904
'VolumeSet',
904905
'Mute',
906+
'Unmute',
905907
]),
906908
'SupportsContentUploading': True,
907909
'SupportsMediaControl': True,
@@ -1785,6 +1787,122 @@
17851787
}),
17861788
'user_id': 'USER-UUID-TWO',
17871789
}),
1790+
dict({
1791+
'capabilities': dict({
1792+
'DeviceProfile': None,
1793+
'MessageCallbackUrl': 'string',
1794+
'PlayableMediaTypes': list([
1795+
'Video',
1796+
]),
1797+
'SupportedCommands': list([
1798+
'Mute',
1799+
'VolumeSet',
1800+
'PlayMediaSource',
1801+
]),
1802+
'SupportsContentUploading': False,
1803+
'SupportsMediaControl': True,
1804+
'SupportsPersistentIdentifier': False,
1805+
'SupportsSync': False,
1806+
}),
1807+
'client_name': 'Test Client Five',
1808+
'client_version': '1.0.0',
1809+
'device_id': 'DEVICE-UUID-FIVE',
1810+
'device_name': 'JELLYFIN-DEVICE-FIVE',
1811+
'id': 'SESSION-UUID-FIVE',
1812+
'now_playing': dict({
1813+
'Id': 'VIDEO-UUID-FIVE',
1814+
'ImageTags': dict({
1815+
'Primary': 'tag',
1816+
}),
1817+
'IndexNumber': 1,
1818+
'Name': 'TEST VIDEO',
1819+
'ParentIndexNumber': 1,
1820+
'PrimaryImageAspectRatio': 1,
1821+
'RunTimeTicks': 600000000,
1822+
'SeriesName': 'TEST SERIES',
1823+
'ServerId': 'SERVER-UUID',
1824+
'Type': 'Episode',
1825+
'UserData': dict({
1826+
'IsFavorite': False,
1827+
'Key': 'string',
1828+
'PlayCount': 0,
1829+
'PlaybackPositionTicks': 0,
1830+
'Played': False,
1831+
}),
1832+
}),
1833+
'play_state': dict({
1834+
'AudioStreamIndex': 0,
1835+
'CanSeek': True,
1836+
'IsMuted': False,
1837+
'IsPaused': False,
1838+
'LiveStreamId': 'string',
1839+
'MediaSourceId': 'string',
1840+
'PlayMethod': 'Transcode',
1841+
'PositionTicks': 100000000,
1842+
'RepeatMode': 'RepeatNone',
1843+
'SubtitleStreamIndex': 0,
1844+
'VolumeLevel': 50,
1845+
}),
1846+
'user_id': 'USER-UUID',
1847+
}),
1848+
dict({
1849+
'capabilities': dict({
1850+
'DeviceProfile': None,
1851+
'MessageCallbackUrl': 'string',
1852+
'PlayableMediaTypes': list([
1853+
'Video',
1854+
]),
1855+
'SupportedCommands': list([
1856+
'Unmute',
1857+
'VolumeSet',
1858+
'PlayMediaSource',
1859+
]),
1860+
'SupportsContentUploading': False,
1861+
'SupportsMediaControl': True,
1862+
'SupportsPersistentIdentifier': False,
1863+
'SupportsSync': False,
1864+
}),
1865+
'client_name': 'Test Client Six',
1866+
'client_version': '1.0.0',
1867+
'device_id': 'DEVICE-UUID-SIX',
1868+
'device_name': 'JELLYFIN-DEVICE-SIX',
1869+
'id': 'SESSION-UUID-SIX',
1870+
'now_playing': dict({
1871+
'Id': 'VIDEO-UUID-SIX',
1872+
'ImageTags': dict({
1873+
'Primary': 'tag',
1874+
}),
1875+
'IndexNumber': 1,
1876+
'Name': 'TEST VIDEO',
1877+
'ParentIndexNumber': 1,
1878+
'PrimaryImageAspectRatio': 1,
1879+
'RunTimeTicks': 600000000,
1880+
'SeriesName': 'TEST SERIES',
1881+
'ServerId': 'SERVER-UUID',
1882+
'Type': 'Episode',
1883+
'UserData': dict({
1884+
'IsFavorite': False,
1885+
'Key': 'string',
1886+
'PlayCount': 0,
1887+
'PlaybackPositionTicks': 0,
1888+
'Played': False,
1889+
}),
1890+
}),
1891+
'play_state': dict({
1892+
'AudioStreamIndex': 0,
1893+
'CanSeek': True,
1894+
'IsMuted': False,
1895+
'IsPaused': False,
1896+
'LiveStreamId': 'string',
1897+
'MediaSourceId': 'string',
1898+
'PlayMethod': 'Transcode',
1899+
'PositionTicks': 100000000,
1900+
'RepeatMode': 'RepeatNone',
1901+
'SubtitleStreamIndex': 0,
1902+
'VolumeLevel': 50,
1903+
}),
1904+
'user_id': 'USER-UUID',
1905+
}),
17881906
]),
17891907
})
17901908
# ---

0 commit comments

Comments
 (0)