From 4ebcb70c8367163bdf9f8ef84c944c437867b5da Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Sun, 18 Sep 2022 13:01:18 +0200 Subject: [PATCH] [jellyfin] add play by id channels and update sdk (#13389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [jellyfin] add play by id channels and update sdk * [jellyfin] add missed Playing Item Id channel Signed-off-by: Miguel Álvarez --- .../org.openhab.binding.jellyfin/README.md | 8 +- bundles/org.openhab.binding.jellyfin/pom.xml | 8 +- .../internal/JellyfinBindingConstants.java | 6 +- .../JellyfinServerDiscoveryService.java | 4 +- .../handler/JellyfinClientHandler.java | 169 ++++++++++++++---- .../handler/JellyfinServerHandler.java | 31 +++- .../resources/OH-INF/i18n/jellyfin.properties | 10 ++ .../resources/OH-INF/thing/thing-types.xml | 31 ++++ 8 files changed, 214 insertions(+), 53 deletions(-) diff --git a/bundles/org.openhab.binding.jellyfin/README.md b/bundles/org.openhab.binding.jellyfin/README.md index 70c7ba8bb..497d3024c 100644 --- a/bundles/org.openhab.binding.jellyfin/README.md +++ b/bundles/org.openhab.binding.jellyfin/README.md @@ -4,6 +4,7 @@ This is the binding for [Jellyfin](https://jellyfin.org) the volunteer-built med Stream to any device from your own server, with no strings attached. Your media, your server, your way. This binding allows connect to Jellyfin clients that supports remote control, it's build on top of the official Jellyfin kotlin sdk. +Compatible with Jellyfin servers in version 10.8.x. ## Supported Things @@ -48,6 +49,7 @@ In order to assist you with this process the binding expose a simple login form |----------|--------|------------------------------| | send-notification | String | Display message in client | | media-control | Player | Control media playback | +| playing-item-id | String | Id of the item currently playing (readonly) | | playing-item-name | String | Name of the item currently playing (readonly) | | playing-item-series-name | String | Name of the item's series currently playing, only have value when item is an episode (readonly) | | playing-item-season-name | String | Name of the item's season currently playing, only have value when item is an episode (readonly) | @@ -62,7 +64,10 @@ In order to assist you with this process the binding expose a simple login form | play-next-by-terms | String | Add to playback queue as next by terms, works for series, episodes and movies; terms search is explained bellow | | play-last-by-terms | String | Add to playback queue as last by terms, works for series, episodes and movies; terms search is explained bellow | | browse-by-terms | String | Browse media by terms, works for series, episodes and movies; terms search is explained bellow | - +| play-by-id | String | Play media by id, works for series, episodes and movies; id search is explained bellow | +| play-next-by-id | String | Add to playback queue as next by id, works for series, episodes and movies | +| play-last-by-id | String | Add to playback queue as last by id, works for series, episodes and movies | +| browse-by-id | String | Browse media by id, works for series, episodes and movies | ### Terms search: The terms search has a default behavior that can be modified sending some predefined prefixes. @@ -106,6 +111,7 @@ Thing jellyfin:client:exampleServerId: "Jellyfin Android cli ``` String strJellyfinAndroidSendNotification { channel="jellyfin:client:exampleServerId::send-notification " } Player plJellyfinAndroidMediaControl { channel="jellyfin:client:exampleServerId::media-control" } +String strJellyfinAndroidPlayingItemId { channel="jellyfin:client:exampleServerId::playing-item-id" } String strJellyfinAndroidPlayingItemName { channel="jellyfin:client:exampleServerId::playing-item-name" } String strJellyfinAndroidPlayingItemSeriesName { channel="jellyfin:client:exampleServerId::playing-item-series-name" } String strJellyfinAndroidPlayingItemSeasonName { channel="jellyfin:client:exampleServerId::playing-item-season-name" } diff --git a/bundles/org.openhab.binding.jellyfin/pom.xml b/bundles/org.openhab.binding.jellyfin/pom.xml index 51491b7a7..c644c853a 100644 --- a/bundles/org.openhab.binding.jellyfin/pom.xml +++ b/bundles/org.openhab.binding.jellyfin/pom.xml @@ -21,17 +21,17 @@ org.jellyfin.sdk jellyfin-core-jvm - 1.2.0 + 1.3.5 org.jellyfin.sdk jellyfin-api-jvm - 1.2.0 + 1.3.5 org.jellyfin.sdk jellyfin-model-jvm - 1.2.0 + 1.3.5 io.ktor @@ -90,7 +90,7 @@ org.jetbrains.kotlinx kotlinx-coroutines-core-jvm - 1.6.1 + 1.6.2 compile diff --git a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/JellyfinBindingConstants.java b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/JellyfinBindingConstants.java index 9a0609828..998429bf0 100644 --- a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/JellyfinBindingConstants.java +++ b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/JellyfinBindingConstants.java @@ -37,6 +37,7 @@ public class JellyfinBindingConstants { public static final String SEND_NOTIFICATION_CHANNEL = "send-notification"; public static final String MEDIA_CONTROL_CHANNEL = "media-control"; public static final String PLAYING_ITEM_PERCENTAGE_CHANNEL = "playing-item-percentage"; + public static final String PLAYING_ITEM_ID_CHANNEL = "playing-item-id"; public static final String PLAYING_ITEM_NAME_CHANNEL = "playing-item-name"; public static final String PLAYING_ITEM_SERIES_NAME_CHANNEL = "playing-item-series-name"; public static final String PLAYING_ITEM_SEASON_NAME_CHANNEL = "playing-item-season-name"; @@ -50,7 +51,10 @@ public class JellyfinBindingConstants { public static final String PLAY_NEXT_BY_TERMS_CHANNEL = "play-next-by-terms"; public static final String PLAY_LAST_BY_TERMS_CHANNEL = "play-last-by-terms"; public static final String BROWSE_ITEM_BY_TERMS_CHANNEL = "browse-by-terms"; - + public static final String PLAY_BY_ID_CHANNEL = "play-by-id"; + public static final String PLAY_NEXT_BY_ID_CHANNEL = "play-next-by-id"; + public static final String PLAY_LAST_BY_ID_CHANNEL = "play-last-by-id"; + public static final String BROWSE_ITEM_BY_ID_CHANNEL = "browse-by-id"; // Discovery public static final int DISCOVERY_RESULT_TTL_SEC = 600; } diff --git a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/discovery/JellyfinServerDiscoveryService.java b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/discovery/JellyfinServerDiscoveryService.java index 3bbc4b770..0b98976db 100644 --- a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/discovery/JellyfinServerDiscoveryService.java +++ b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/discovery/JellyfinServerDiscoveryService.java @@ -28,6 +28,7 @@ import org.jellyfin.sdk.JellyfinOptions; import org.jellyfin.sdk.api.client.exception.ApiClientException; import org.jellyfin.sdk.api.operations.SystemApi; import org.jellyfin.sdk.compatibility.JavaFlow; +import org.jellyfin.sdk.compatibility.JavaFlow.FlowJob; import org.jellyfin.sdk.model.ClientInfo; import org.jellyfin.sdk.model.DeviceInfo; import org.jellyfin.sdk.model.api.PublicSystemInfo; @@ -53,7 +54,8 @@ import org.slf4j.LoggerFactory; @Component(service = DiscoveryService.class, configurationPid = "discovery.jellyfin") public class JellyfinServerDiscoveryService extends AbstractDiscoveryService { private final Logger logger = LoggerFactory.getLogger(JellyfinServerDiscoveryService.class); - private JavaFlow.@Nullable FlowJob cancelDiscovery; + @Nullable + private FlowJob cancelDiscovery; public JellyfinServerDiscoveryService() throws IllegalArgumentException { super(Set.of(THING_TYPE_CLIENT), 60); diff --git a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinClientHandler.java b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinClientHandler.java index 87c6160e0..b4e1ac4ad 100644 --- a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinClientHandler.java +++ b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinClientHandler.java @@ -12,10 +12,12 @@ */ package org.openhab.binding.jellyfin.internal.handler; +import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.BROWSE_ITEM_BY_ID_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.BROWSE_ITEM_BY_TERMS_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.MEDIA_CONTROL_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_EPISODE_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_GENRES_CHANNEL; +import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_ID_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_NAME_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_PERCENTAGE_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_SEASON_CHANNEL; @@ -24,13 +26,18 @@ import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLA import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_SERIES_NAME_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_TOTAL_SECOND_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_TYPE_CHANNEL; +import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_BY_ID_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_BY_TERMS_CHANNEL; +import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_LAST_BY_ID_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_LAST_BY_TERMS_CHANNEL; +import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_NEXT_BY_ID_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_NEXT_BY_TERMS_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.SEND_NOTIFICATION_CHANNEL; +import java.math.BigInteger; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -39,6 +46,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.jellyfin.sdk.api.client.exception.ApiClientException; import org.jellyfin.sdk.model.api.BaseItemDto; +import org.jellyfin.sdk.model.api.BaseItemKind; import org.jellyfin.sdk.model.api.PlayCommand; import org.jellyfin.sdk.model.api.PlayerStateInfo; import org.jellyfin.sdk.model.api.PlaystateCommand; @@ -141,6 +149,55 @@ public class JellyfinClientHandler extends BaseThingHandler { } runItemSearch(command.toFullString(), null); break; + case PLAY_BY_ID_CHANNEL: + if (command instanceof RefreshType) { + return; + } + UUID itemUUID; + try { + itemUUID = parseItemUUID(command); + } catch (NumberFormatException e) { + logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command); + return; + } + runItemById(itemUUID, PlayCommand.PLAY_NOW); + break; + case PLAY_NEXT_BY_ID_CHANNEL: + if (command instanceof RefreshType) { + return; + } + try { + itemUUID = parseItemUUID(command); + } catch (NumberFormatException e) { + logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command); + return; + } + runItemById(itemUUID, PlayCommand.PLAY_NEXT); + break; + case PLAY_LAST_BY_ID_CHANNEL: + if (command instanceof RefreshType) { + return; + } + try { + itemUUID = parseItemUUID(command); + } catch (NumberFormatException e) { + logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command); + return; + } + runItemById(itemUUID, PlayCommand.PLAY_LAST); + break; + case BROWSE_ITEM_BY_ID_CHANNEL: + if (command instanceof RefreshType) { + return; + } + try { + itemUUID = parseItemUUID(command); + } catch (NumberFormatException e) { + logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command); + return; + } + runItemById(itemUUID, null); + break; case PLAYING_ITEM_SECOND_CHANNEL: if (command instanceof RefreshType) { refreshState(); @@ -161,6 +218,7 @@ public class JellyfinClientHandler extends BaseThingHandler { } seekToPercentage(Integer.parseInt(command.toFullString())); break; + case PLAYING_ITEM_ID_CHANNEL: case PLAYING_ITEM_NAME_CHANNEL: case PLAYING_ITEM_GENRES_CHANNEL: case PLAYING_ITEM_SEASON_CHANNEL: @@ -183,6 +241,13 @@ public class JellyfinClientHandler extends BaseThingHandler { } } + private UUID parseItemUUID(Command command) throws NumberFormatException { + var itemId = command.toFullString().replace("-", ""); + UUID itemUUID = new UUID(new BigInteger(itemId.substring(0, 16), 16).longValue(), + new BigInteger(itemId.substring(16), 16).longValue()); + return itemUUID; + } + @Override public void dispose() { super.dispose(); @@ -236,6 +301,14 @@ public class JellyfinClientHandler extends BaseThingHandler { cleanChannel(PLAYING_ITEM_TOTAL_SECOND_CHANNEL); } } + if (isLinked(PLAYING_ITEM_ID_CHANNEL)) { + if (playingItem != null) { + updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_ID_CHANNEL), + new StringType(playingItem.getId().toString())); + } else { + cleanChannel(PLAYING_ITEM_ID_CHANNEL); + } + } if (isLinked(PLAYING_ITEM_NAME_CHANNEL)) { if (playingItem != null) { updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_NAME_CHANNEL), @@ -253,7 +326,7 @@ public class JellyfinClientHandler extends BaseThingHandler { } } if (isLinked(PLAYING_ITEM_SEASON_NAME_CHANNEL)) { - if (playingItem != null && "Episode".equals(playingItem.getType())) { + if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) { updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_NAME_CHANNEL), new StringType(playingItem.getSeasonName())); } else { @@ -261,7 +334,7 @@ public class JellyfinClientHandler extends BaseThingHandler { } } if (isLinked(PLAYING_ITEM_SEASON_CHANNEL)) { - if (playingItem != null && "Episode".equals(playingItem.getType())) { + if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) { updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_CHANNEL), new DecimalType(Objects.requireNonNull(playingItem.getParentIndexNumber()))); } else { @@ -269,7 +342,7 @@ public class JellyfinClientHandler extends BaseThingHandler { } } if (isLinked(PLAYING_ITEM_EPISODE_CHANNEL)) { - if (playingItem != null && "Episode".equals(playingItem.getType())) { + if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) { updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_EPISODE_CHANNEL), new DecimalType(Objects.requireNonNull(playingItem.getIndexNumber()))); } else { @@ -287,7 +360,7 @@ public class JellyfinClientHandler extends BaseThingHandler { if (isLinked(PLAYING_ITEM_TYPE_CHANNEL)) { if (playingItem != null) { updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TYPE_CHANNEL), - new StringType(playingItem.getType())); + new StringType(playingItem.getType().toString())); } else { cleanChannel(PLAYING_ITEM_TYPE_CHANNEL); } @@ -322,9 +395,10 @@ public class JellyfinClientHandler extends BaseThingHandler { private void runItemSearchByType(String terms, @Nullable PlayCommand playCommand, boolean movieSearchEnabled, boolean seriesSearchEnabled, boolean episodeSearchEnabled) throws SyncCallback.SyncCallbackError, ApiClientException { - var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, "Series", null) : null; - var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, "Movie", null) : null; - var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, "Episode", null) : null; + var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.SERIES, null) : null; + var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.MOVIE, null) : null; + var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.EPISODE, null) + : null; if (movieItem != null) { logger.debug("Found movie: '{}'", movieItem.getName()); } @@ -337,30 +411,7 @@ public class JellyfinClientHandler extends BaseThingHandler { if (movieItem != null) { runItem(movieItem, playCommand); } else if (seriesItem != null) { - if (playCommand != null) { - var resumeEpisodeItem = getServerHandler().getSeriesResumeItem(seriesItem.getId()); - var nextUpEpisodeItem = getServerHandler().getSeriesNextUpItem(seriesItem.getId()); - var firstEpisodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), 1, 1); - if (resumeEpisodeItem != null) { - logger.debug("Resuming series '{}' episode '{}'", seriesItem.getName(), - resumeEpisodeItem.getName()); - playItem(resumeEpisodeItem, playCommand, - Objects.requireNonNull(resumeEpisodeItem.getUserData()).getPlaybackPositionTicks()); - } else if (nextUpEpisodeItem != null) { - logger.debug("Playing next series '{}' episode '{}'", seriesItem.getName(), - nextUpEpisodeItem.getName()); - playItem(nextUpEpisodeItem, playCommand); - } else if (firstEpisodeItem != null) { - logger.debug("Playing series '{}' first episode '{}'", seriesItem.getName(), - firstEpisodeItem.getName()); - playItem(firstEpisodeItem, playCommand); - } else { - logger.warn("Unable to found episode for series"); - } - } else { - logger.debug("Browse series '{}'", seriesItem.getName()); - browseItem(seriesItem); - } + runSeriesItem(seriesItem, playCommand); } else if (episodeItem != null) { runItem(episodeItem, playCommand); } else { @@ -368,10 +419,37 @@ public class JellyfinClientHandler extends BaseThingHandler { } } + private void runSeriesItem(BaseItemDto seriesItem, @Nullable PlayCommand playCommand) + throws SyncCallback.SyncCallbackError, ApiClientException { + if (playCommand != null) { + var resumeEpisodeItem = getServerHandler().getSeriesResumeItem(seriesItem.getId()); + var nextUpEpisodeItem = getServerHandler().getSeriesNextUpItem(seriesItem.getId()); + var firstEpisodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), 1, 1); + if (resumeEpisodeItem != null) { + logger.debug("Resuming series '{}' episode '{}'", seriesItem.getName(), resumeEpisodeItem.getName()); + playItem(resumeEpisodeItem, playCommand, + Objects.requireNonNull(resumeEpisodeItem.getUserData()).getPlaybackPositionTicks()); + } else if (nextUpEpisodeItem != null) { + logger.debug("Playing next series '{}' episode '{}'", seriesItem.getName(), + nextUpEpisodeItem.getName()); + playItem(nextUpEpisodeItem, playCommand); + } else if (firstEpisodeItem != null) { + logger.debug("Playing series '{}' first episode '{}'", seriesItem.getName(), + firstEpisodeItem.getName()); + playItem(firstEpisodeItem, playCommand); + } else { + logger.warn("Unable to found episode for series"); + } + } else { + logger.debug("Browse series '{}'", seriesItem.getName()); + browseItem(seriesItem); + } + } + private void runSeriesEpisode(String terms, int season, int episode, @Nullable PlayCommand playCommand) throws SyncCallback.SyncCallbackError, ApiClientException { logger.debug("{} series episode mode", playCommand != null ? "Play" : "Browse"); - var seriesItem = getServerHandler().searchItem(terms, "Series", null); + var seriesItem = getServerHandler().searchItem(terms, BaseItemKind.SERIES, null); if (seriesItem != null) { logger.debug("Searching series {} episode {}x{}", seriesItem.getName(), season, episode); var episodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), season, episode); @@ -388,8 +466,8 @@ public class JellyfinClientHandler extends BaseThingHandler { private void runItem(BaseItemDto item, @Nullable PlayCommand playCommand) throws SyncCallback.SyncCallbackError, ApiClientException { var itemType = Objects.requireNonNull(item.getType()); - logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toLowerCase(), - "Episode".equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName()); + logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toString().toLowerCase(), + BaseItemKind.EPISODE.equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName()); if (playCommand == null) { browseItem(item); } else { @@ -423,6 +501,20 @@ public class JellyfinClientHandler extends BaseThingHandler { getServerHandler().playItem(lastSessionId, playCommand, item.getId().toString(), startPositionTicks); } + private void runItemById(UUID itemId, @Nullable PlayCommand playCommand) + throws SyncCallback.SyncCallbackError, ApiClientException { + var item = getServerHandler().getItem(itemId, null); + if (item == null) { + logger.warn("Unable to find item with id: {}", itemId); + return; + } + if (BaseItemKind.SERIES.equals(item.getType())) { + runSeriesItem(item, playCommand); + } else { + runItem(item, playCommand); + } + } + private void browseItem(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException { if (stopCurrentPlayback()) { cancelDelayedCommand(); @@ -517,10 +609,11 @@ public class JellyfinClientHandler extends BaseThingHandler { } private void cleanChannels() { - List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_NAME_CHANNEL, - PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL, PLAYING_ITEM_SEASON_CHANNEL, - PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL, PLAYING_ITEM_TYPE_CHANNEL, - PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL).forEach(this::cleanChannel); + List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_ID_CHANNEL, + PLAYING_ITEM_NAME_CHANNEL, PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL, + PLAYING_ITEM_SEASON_CHANNEL, PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL, + PLAYING_ITEM_TYPE_CHANNEL, PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL) + .forEach(this::cleanChannel); } private void cleanChannel(String channelId) { diff --git a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinServerHandler.java b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinServerHandler.java index 38947a58f..2172001fa 100644 --- a/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinServerHandler.java +++ b/bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinServerHandler.java @@ -40,6 +40,7 @@ import org.jellyfin.sdk.model.api.AuthenticateUserByName; import org.jellyfin.sdk.model.api.AuthenticationResult; import org.jellyfin.sdk.model.api.BaseItemDto; import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult; +import org.jellyfin.sdk.model.api.BaseItemKind; import org.jellyfin.sdk.model.api.ItemFields; import org.jellyfin.sdk.model.api.MessageCommand; import org.jellyfin.sdk.model.api.PlayCommand; @@ -271,7 +272,7 @@ public class JellyfinServerHandler extends BaseBridgeHandler { awaiter.awaitResponse(); } - public void browseToItem(String sessionId, String itemType, String itemId, String itemName) + public void browseToItem(String sessionId, BaseItemKind itemType, String itemId, String itemName) throws SyncCallback.SyncCallbackError, ApiClientException { var awaiter = new EmptySyncResponse(); new SessionApi(jellyApiClient).displayContent(sessionId, itemType, itemId, itemName, awaiter); @@ -287,7 +288,7 @@ public class JellyfinServerHandler extends BaseBridgeHandler { throws SyncCallback.SyncCallbackError, ApiClientException { var asyncContinuation = new SyncResponse(); new TvShowsApi(jellyApiClient).getNextUp(jellyApiClient.getUserId(), null, limit, null, seriesId.toString(), - null, null, null, null, null, null, null, asyncContinuation); + null, null, null, null, null, null, null, null, null, asyncContinuation); var result = asyncContinuation.awaitContent(); return Objects.requireNonNull(result.getItems()); } @@ -301,7 +302,8 @@ public class JellyfinServerHandler extends BaseBridgeHandler { throws SyncCallback.SyncCallbackError, ApiClientException { var asyncContinuation = new SyncResponse(); new ItemsApi(jellyApiClient).getResumeItems(Objects.requireNonNull(jellyApiClient.getUserId()), null, limit, - null, seriesId, null, null, true, null, null, null, List.of("Episode"), null, null, asyncContinuation); + null, seriesId, null, null, true, null, null, null, List.of(BaseItemKind.EPISODE), null, null, null, + asyncContinuation); var result = asyncContinuation.awaitContent(); return Objects.requireNonNull(result.getItems()); } @@ -320,21 +322,34 @@ public class JellyfinServerHandler extends BaseBridgeHandler { return Objects.requireNonNull(result.getItems()); } - public @Nullable BaseItemDto searchItem(@Nullable String searchTerm, @Nullable String itemType, + public @Nullable BaseItemDto getItem(UUID id, @Nullable List fields) + throws SyncCallback.SyncCallbackError, ApiClientException { + var asyncContinuation = new SyncResponse(); + new ItemsApi(jellyApiClient).getItems(jellyApiClient.getUserId(), null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, 1, true, null, null, null, fields, null, null, null, + null, null, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, null, + null, null, null, null, null, List.of(id), null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, false, false, asyncContinuation); + var response = asyncContinuation.awaitContent(); + return Objects.requireNonNull(response.getItems()).stream().findFirst().orElse(null); + } + + public @Nullable BaseItemDto searchItem(@Nullable String searchTerm, @Nullable BaseItemKind itemType, @Nullable List fields) throws SyncCallback.SyncCallbackError, ApiClientException { return searchItems(searchTerm, itemType, fields, 1).stream().findFirst().orElse(null); } - public List searchItems(@Nullable String searchTerm, @Nullable String itemType, + public List searchItems(@Nullable String searchTerm, @Nullable BaseItemKind itemType, @Nullable List fields, int limit) throws SyncCallback.SyncCallbackError, ApiClientException { var asyncContinuation = new SyncResponse(); var itemTypes = itemType != null ? List.of(itemType) : null; new ItemsApi(jellyApiClient).getItems(jellyApiClient.getUserId(), null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, limit, true, searchTerm, null, null, fields, null, itemTypes, null, null, null, null, - null, null, null, null, null, null, null, 1, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, limit, true, searchTerm, null, null, fields, null, + itemTypes, null, null, null, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, false, false, asyncContinuation); + null, null, null, null, null, null, null, null, null, false, false, asyncContinuation); var response = asyncContinuation.awaitContent(); return Objects.requireNonNull(response.getItems()); } diff --git a/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/i18n/jellyfin.properties b/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/i18n/jellyfin.properties index db0563513..5ffb84755 100644 --- a/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/i18n/jellyfin.properties +++ b/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/i18n/jellyfin.properties @@ -29,18 +29,28 @@ thing-type.config.jellyfin.server.userId.description = The user id # channel types +channel-type.jellyfin.browse-by-id-channel.label = Browse By Id +channel-type.jellyfin.browse-by-id-channel.description = Browse media by id channel-type.jellyfin.browse-by-terms-channel.label = Browse By Terms channel-type.jellyfin.browse-by-terms-channel.description = Browse media by terms, works for series, episodes and movies +channel-type.jellyfin.play-by-id-channel.label = Play By Id +channel-type.jellyfin.play-by-id-channel.description = Play media by id channel-type.jellyfin.play-by-terms-channel.label = Play By Terms channel-type.jellyfin.play-by-terms-channel.description = Play media by terms, works for series, episodes and movies +channel-type.jellyfin.play-last-by-id-channel.label = Play Last By Id +channel-type.jellyfin.play-last-by-id-channel.description = Add to playback queue as last by id channel-type.jellyfin.play-last-by-terms-channel.label = Play Last By Terms channel-type.jellyfin.play-last-by-terms-channel.description = Add to playback queue as last by terms; works for series, episodes and movies +channel-type.jellyfin.play-next-by-id-channel.label = Play Next By Id +channel-type.jellyfin.play-next-by-id-channel.description = Add to playback queue as next by id channel-type.jellyfin.play-next-by-terms-channel.label = Play Next By Terms channel-type.jellyfin.play-next-by-terms-channel.description = Add to playback queue as next by terms; works for series, episodes and movies channel-type.jellyfin.playing-item-episode-channel.label = Playing Item Episode channel-type.jellyfin.playing-item-episode-channel.description = Number of the episode item currently playing, only have value when item is an episode channel-type.jellyfin.playing-item-genders-channel.label = Playing Item Genders channel-type.jellyfin.playing-item-genders-channel.description = Coma separate list genders of the item currently playing +channel-type.jellyfin.playing-item-id-channel.label = Playing Item Id +channel-type.jellyfin.playing-item-id-channel.description = Id of the item currently playing channel-type.jellyfin.playing-item-name-channel.label = Playing Item Name channel-type.jellyfin.playing-item-name-channel.description = Name of the item currently playing channel-type.jellyfin.playing-item-percentage-channel.label = Playing Item Percentage diff --git a/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/thing/thing-types.xml index ace51e62e..3147794e2 100644 --- a/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/thing/thing-types.xml @@ -55,6 +55,7 @@ + @@ -69,6 +70,10 @@ + + + + @@ -81,6 +86,12 @@ Send notification to the client + + String + + Id of the item currently playing + + String @@ -159,4 +170,24 @@ Browse media by terms, works for series, episodes and movies + + String + + Play media by id + + + String + + Add to playback queue as next by id + + + String + + Add to playback queue as last by id + + + String + + Browse media by id +