]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jellyfin] add play by id channels and update sdk (#13389)
authorGiviMAD <GiviMAD@users.noreply.github.com>
Sun, 18 Sep 2022 11:01:18 +0000 (13:01 +0200)
committerGitHub <noreply@github.com>
Sun, 18 Sep 2022 11:01:18 +0000 (13:01 +0200)
* [jellyfin] add play by id channels and update sdk
* [jellyfin] add missed Playing Item Id channel

Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
bundles/org.openhab.binding.jellyfin/README.md
bundles/org.openhab.binding.jellyfin/pom.xml
bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/JellyfinBindingConstants.java
bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/discovery/JellyfinServerDiscoveryService.java
bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinClientHandler.java
bundles/org.openhab.binding.jellyfin/src/main/java/org/openhab/binding/jellyfin/internal/handler/JellyfinServerHandler.java
bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/i18n/jellyfin.properties
bundles/org.openhab.binding.jellyfin/src/main/resources/OH-INF/thing/thing-types.xml

index 70c7ba8bb06e09940d85f7564647ece2dd4bf7d6..497d3024c36ce540b2af9d01b2f4d09f81f69995 100644 (file)
@@ -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_DEVICE_ID> "Jellyfin Android cli
 ```
 String strJellyfinAndroidSendNotification { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:send-notification " }
 Player plJellyfinAndroidMediaControl { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:media-control" }
+String strJellyfinAndroidPlayingItemId { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:playing-item-id" }
 String strJellyfinAndroidPlayingItemName { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:playing-item-name" }
 String strJellyfinAndroidPlayingItemSeriesName { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:playing-item-series-name" }
 String strJellyfinAndroidPlayingItemSeasonName { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:playing-item-season-name" }
index 51491b7a7c75e5377eea462a95eaca9396346964..c644c853a6556d67c7914d35dd0fd62b3346c6ee 100644 (file)
     <dependency>
       <groupId>org.jellyfin.sdk</groupId>
       <artifactId>jellyfin-core-jvm</artifactId>
-      <version>1.2.0</version>
+      <version>1.3.5</version>
     </dependency>
     <dependency>
       <groupId>org.jellyfin.sdk</groupId>
       <artifactId>jellyfin-api-jvm</artifactId>
-      <version>1.2.0</version>
+      <version>1.3.5</version>
     </dependency>
     <dependency>
       <groupId>org.jellyfin.sdk</groupId>
       <artifactId>jellyfin-model-jvm</artifactId>
-      <version>1.2.0</version>
+      <version>1.3.5</version>
     </dependency>
     <dependency>
       <groupId>io.ktor</groupId>
@@ -90,7 +90,7 @@
     <dependency>
       <groupId>org.jetbrains.kotlinx</groupId>
       <artifactId>kotlinx-coroutines-core-jvm</artifactId>
-      <version>1.6.1</version>
+      <version>1.6.2</version>
       <scope>compile</scope>
     </dependency>
     <dependency>
index 9a06098281a14f390857096bff845bc0073948a2..998429bf008dfa83b0e78ed9e76cb4667d2d2a94 100644 (file)
@@ -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;
 }
index 3bbc4b7709afb8a781f06c55fd21711d08b22273..0b98976db484706829d9ec51285ab8f74e845105 100644 (file)
@@ -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);
index 87c6160e0fb6d8f7270c58303cd17dc0c6a1c206..b4e1ac4ad7c068b94c795fde5a4cbebdca6fa1dc 100644 (file)
  */
 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) {
index 38947a58f4d9e88e5655efba89272c8625f33b73..2172001facb7ada06bf17c5eed4f616330dd2e4e 100644 (file)
@@ -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<BaseItemDtoQueryResult>();
         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<BaseItemDtoQueryResult>();
         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<ItemFields> fields)
+            throws SyncCallback.SyncCallbackError, ApiClientException {
+        var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>();
+        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<ItemFields> fields) throws SyncCallback.SyncCallbackError, ApiClientException {
         return searchItems(searchTerm, itemType, fields, 1).stream().findFirst().orElse(null);
     }
 
-    public List<BaseItemDto> searchItems(@Nullable String searchTerm, @Nullable String itemType,
+    public List<BaseItemDto> searchItems(@Nullable String searchTerm, @Nullable BaseItemKind itemType,
             @Nullable List<ItemFields> fields, int limit) throws SyncCallback.SyncCallbackError, ApiClientException {
         var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>();
         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());
     }
index db0563513b28fdb95beeed8f992158bc9cfaeeea..5ffb84755e18b7f192af873422cf4e82629a6dd0 100644 (file)
@@ -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
index ace51e62ec717265e33b38b36f93b5d56430fb48..3147794e252f06e982b738501cc2942d8b7b156d 100644 (file)
@@ -55,6 +55,7 @@
                <channels>
                        <channel id="send-notification" typeId="send-notification-channel"/>
                        <channel id="media-control" typeId="system.media-control"/>
+                       <channel id="playing-item-id" typeId="playing-item-id-channel"/>
                        <channel id="playing-item-name" typeId="playing-item-name-channel"/>
                        <channel id="playing-item-series-name" typeId="playing-item-series-name-channel"/>
                        <channel id="playing-item-season-name" typeId="playing-item-season-name-channel"/>
                        <channel id="play-next-by-terms" typeId="play-next-by-terms-channel"/>
                        <channel id="play-last-by-terms" typeId="play-last-by-terms-channel"/>
                        <channel id="browse-by-terms" typeId="browse-by-terms-channel"/>
+                       <channel id="play-by-id" typeId="play-by-id-channel"/>
+                       <channel id="play-next-by-id" typeId="play-next-by-id-channel"/>
+                       <channel id="play-last-by-id" typeId="play-last-by-id-channel"/>
+                       <channel id="browse-by-id" typeId="browse-by-id-channel"/>
                </channels>
 
                <config-description>
                <label>Send Notification</label>
                <description>Send notification to the client</description>
        </channel-type>
+       <channel-type id="playing-item-id-channel">
+               <item-type>String</item-type>
+               <label>Playing Item Id</label>
+               <description>Id of the item currently playing</description>
+               <state readOnly="true"/>
+       </channel-type>
        <channel-type id="playing-item-name-channel">
                <item-type>String</item-type>
                <label>Playing Item Name</label>
                <label>Browse By Terms</label>
                <description>Browse media by terms, works for series, episodes and movies</description>
        </channel-type>
+       <channel-type id="play-by-id-channel">
+               <item-type>String</item-type>
+               <label>Play By Id</label>
+               <description>Play media by id</description>
+       </channel-type>
+       <channel-type id="play-next-by-id-channel">
+               <item-type>String</item-type>
+               <label>Play Next By Id</label>
+               <description>Add to playback queue as next by id</description>
+       </channel-type>
+       <channel-type id="play-last-by-id-channel">
+               <item-type>String</item-type>
+               <label>Play Last By Id</label>
+               <description>Add to playback queue as last by id</description>
+       </channel-type>
+       <channel-type id="browse-by-id-channel">
+               <item-type>String</item-type>
+               <label>Browse By Id</label>
+               <description>Browse media by id</description>
+       </channel-type>
 </thing:thing-descriptions>