[jellyfin] add play by id channels and update sdk (#13389)

* [jellyfin] add play by id channels and update sdk
* [jellyfin] add missed Playing Item Id channel

Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD 2022-09-18 13:01:18 +02:00 committed by GitHub
parent b2d9fe5c0a
commit 4ebcb70c83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 214 additions and 53 deletions

View 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. Stream to any device from your own server, with no strings attached.
Your media, your server, your way. 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. 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 ## 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 | | send-notification | String | Display message in client |
| media-control | Player | Control media playback | | 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-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-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) | | 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-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 | | 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 | | 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: ### Terms search:
The terms search has a default behavior that can be modified sending some predefined prefixes. 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 " } String strJellyfinAndroidSendNotification { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:send-notification " }
Player plJellyfinAndroidMediaControl { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:media-control" } 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 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 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" } String strJellyfinAndroidPlayingItemSeasonName { channel="jellyfin:client:exampleServerId:<JELLYFIN_DEVICE_ID>:playing-item-season-name" }

View File

@ -21,17 +21,17 @@
<dependency> <dependency>
<groupId>org.jellyfin.sdk</groupId> <groupId>org.jellyfin.sdk</groupId>
<artifactId>jellyfin-core-jvm</artifactId> <artifactId>jellyfin-core-jvm</artifactId>
<version>1.2.0</version> <version>1.3.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jellyfin.sdk</groupId> <groupId>org.jellyfin.sdk</groupId>
<artifactId>jellyfin-api-jvm</artifactId> <artifactId>jellyfin-api-jvm</artifactId>
<version>1.2.0</version> <version>1.3.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jellyfin.sdk</groupId> <groupId>org.jellyfin.sdk</groupId>
<artifactId>jellyfin-model-jvm</artifactId> <artifactId>jellyfin-model-jvm</artifactId>
<version>1.2.0</version> <version>1.3.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.ktor</groupId> <groupId>io.ktor</groupId>
@ -90,7 +90,7 @@
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core-jvm</artifactId> <artifactId>kotlinx-coroutines-core-jvm</artifactId>
<version>1.6.1</version> <version>1.6.2</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -37,6 +37,7 @@ public class JellyfinBindingConstants {
public static final String SEND_NOTIFICATION_CHANNEL = "send-notification"; public static final String SEND_NOTIFICATION_CHANNEL = "send-notification";
public static final String MEDIA_CONTROL_CHANNEL = "media-control"; 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_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_NAME_CHANNEL = "playing-item-name";
public static final String PLAYING_ITEM_SERIES_NAME_CHANNEL = "playing-item-series-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"; 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_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 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 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 // Discovery
public static final int DISCOVERY_RESULT_TTL_SEC = 600; public static final int DISCOVERY_RESULT_TTL_SEC = 600;
} }

View File

@ -28,6 +28,7 @@ import org.jellyfin.sdk.JellyfinOptions;
import org.jellyfin.sdk.api.client.exception.ApiClientException; import org.jellyfin.sdk.api.client.exception.ApiClientException;
import org.jellyfin.sdk.api.operations.SystemApi; import org.jellyfin.sdk.api.operations.SystemApi;
import org.jellyfin.sdk.compatibility.JavaFlow; import org.jellyfin.sdk.compatibility.JavaFlow;
import org.jellyfin.sdk.compatibility.JavaFlow.FlowJob;
import org.jellyfin.sdk.model.ClientInfo; import org.jellyfin.sdk.model.ClientInfo;
import org.jellyfin.sdk.model.DeviceInfo; import org.jellyfin.sdk.model.DeviceInfo;
import org.jellyfin.sdk.model.api.PublicSystemInfo; import org.jellyfin.sdk.model.api.PublicSystemInfo;
@ -53,7 +54,8 @@ import org.slf4j.LoggerFactory;
@Component(service = DiscoveryService.class, configurationPid = "discovery.jellyfin") @Component(service = DiscoveryService.class, configurationPid = "discovery.jellyfin")
public class JellyfinServerDiscoveryService extends AbstractDiscoveryService { public class JellyfinServerDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(JellyfinServerDiscoveryService.class); private final Logger logger = LoggerFactory.getLogger(JellyfinServerDiscoveryService.class);
private JavaFlow.@Nullable FlowJob cancelDiscovery; @Nullable
private FlowJob cancelDiscovery;
public JellyfinServerDiscoveryService() throws IllegalArgumentException { public JellyfinServerDiscoveryService() throws IllegalArgumentException {
super(Set.of(THING_TYPE_CLIENT), 60); super(Set.of(THING_TYPE_CLIENT), 60);

View File

@ -12,10 +12,12 @@
*/ */
package org.openhab.binding.jellyfin.internal.handler; 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.BROWSE_ITEM_BY_TERMS_CHANNEL;
import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.MEDIA_CONTROL_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_EPISODE_CHANNEL;
import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_GENRES_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_NAME_CHANNEL;
import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_PERCENTAGE_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; 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_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_TOTAL_SECOND_CHANNEL;
import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_TYPE_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_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_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.PLAY_NEXT_BY_TERMS_CHANNEL;
import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.SEND_NOTIFICATION_CHANNEL; import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.SEND_NOTIFICATION_CHANNEL;
import java.math.BigInteger;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -39,6 +46,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.jellyfin.sdk.api.client.exception.ApiClientException; import org.jellyfin.sdk.api.client.exception.ApiClientException;
import org.jellyfin.sdk.model.api.BaseItemDto; 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.PlayCommand;
import org.jellyfin.sdk.model.api.PlayerStateInfo; import org.jellyfin.sdk.model.api.PlayerStateInfo;
import org.jellyfin.sdk.model.api.PlaystateCommand; import org.jellyfin.sdk.model.api.PlaystateCommand;
@ -141,6 +149,55 @@ public class JellyfinClientHandler extends BaseThingHandler {
} }
runItemSearch(command.toFullString(), null); runItemSearch(command.toFullString(), null);
break; 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: case PLAYING_ITEM_SECOND_CHANNEL:
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
refreshState(); refreshState();
@ -161,6 +218,7 @@ public class JellyfinClientHandler extends BaseThingHandler {
} }
seekToPercentage(Integer.parseInt(command.toFullString())); seekToPercentage(Integer.parseInt(command.toFullString()));
break; break;
case PLAYING_ITEM_ID_CHANNEL:
case PLAYING_ITEM_NAME_CHANNEL: case PLAYING_ITEM_NAME_CHANNEL:
case PLAYING_ITEM_GENRES_CHANNEL: case PLAYING_ITEM_GENRES_CHANNEL:
case PLAYING_ITEM_SEASON_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 @Override
public void dispose() { public void dispose() {
super.dispose(); super.dispose();
@ -236,6 +301,14 @@ public class JellyfinClientHandler extends BaseThingHandler {
cleanChannel(PLAYING_ITEM_TOTAL_SECOND_CHANNEL); 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 (isLinked(PLAYING_ITEM_NAME_CHANNEL)) {
if (playingItem != null) { if (playingItem != null) {
updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_NAME_CHANNEL), 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 (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), updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_NAME_CHANNEL),
new StringType(playingItem.getSeasonName())); new StringType(playingItem.getSeasonName()));
} else { } else {
@ -261,7 +334,7 @@ public class JellyfinClientHandler extends BaseThingHandler {
} }
} }
if (isLinked(PLAYING_ITEM_SEASON_CHANNEL)) { 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), updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_CHANNEL),
new DecimalType(Objects.requireNonNull(playingItem.getParentIndexNumber()))); new DecimalType(Objects.requireNonNull(playingItem.getParentIndexNumber())));
} else { } else {
@ -269,7 +342,7 @@ public class JellyfinClientHandler extends BaseThingHandler {
} }
} }
if (isLinked(PLAYING_ITEM_EPISODE_CHANNEL)) { 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), updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_EPISODE_CHANNEL),
new DecimalType(Objects.requireNonNull(playingItem.getIndexNumber()))); new DecimalType(Objects.requireNonNull(playingItem.getIndexNumber())));
} else { } else {
@ -287,7 +360,7 @@ public class JellyfinClientHandler extends BaseThingHandler {
if (isLinked(PLAYING_ITEM_TYPE_CHANNEL)) { if (isLinked(PLAYING_ITEM_TYPE_CHANNEL)) {
if (playingItem != null) { if (playingItem != null) {
updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TYPE_CHANNEL), updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TYPE_CHANNEL),
new StringType(playingItem.getType())); new StringType(playingItem.getType().toString()));
} else { } else {
cleanChannel(PLAYING_ITEM_TYPE_CHANNEL); cleanChannel(PLAYING_ITEM_TYPE_CHANNEL);
} }
@ -322,9 +395,10 @@ public class JellyfinClientHandler extends BaseThingHandler {
private void runItemSearchByType(String terms, @Nullable PlayCommand playCommand, boolean movieSearchEnabled, private void runItemSearchByType(String terms, @Nullable PlayCommand playCommand, boolean movieSearchEnabled,
boolean seriesSearchEnabled, boolean episodeSearchEnabled) boolean seriesSearchEnabled, boolean episodeSearchEnabled)
throws SyncCallback.SyncCallbackError, ApiClientException { throws SyncCallback.SyncCallbackError, ApiClientException {
var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, "Series", null) : null; var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.SERIES, null) : null;
var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, "Movie", null) : null; var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.MOVIE, null) : null;
var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, "Episode", null) : null; var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.EPISODE, null)
: null;
if (movieItem != null) { if (movieItem != null) {
logger.debug("Found movie: '{}'", movieItem.getName()); logger.debug("Found movie: '{}'", movieItem.getName());
} }
@ -337,30 +411,7 @@ public class JellyfinClientHandler extends BaseThingHandler {
if (movieItem != null) { if (movieItem != null) {
runItem(movieItem, playCommand); runItem(movieItem, playCommand);
} else if (seriesItem != null) { } else if (seriesItem != null) {
if (playCommand != null) { runSeriesItem(seriesItem, playCommand);
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);
}
} else if (episodeItem != null) { } else if (episodeItem != null) {
runItem(episodeItem, playCommand); runItem(episodeItem, playCommand);
} else { } 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) private void runSeriesEpisode(String terms, int season, int episode, @Nullable PlayCommand playCommand)
throws SyncCallback.SyncCallbackError, ApiClientException { throws SyncCallback.SyncCallbackError, ApiClientException {
logger.debug("{} series episode mode", playCommand != null ? "Play" : "Browse"); 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) { if (seriesItem != null) {
logger.debug("Searching series {} episode {}x{}", seriesItem.getName(), season, episode); logger.debug("Searching series {} episode {}x{}", seriesItem.getName(), season, episode);
var episodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), 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) private void runItem(BaseItemDto item, @Nullable PlayCommand playCommand)
throws SyncCallback.SyncCallbackError, ApiClientException { throws SyncCallback.SyncCallbackError, ApiClientException {
var itemType = Objects.requireNonNull(item.getType()); var itemType = Objects.requireNonNull(item.getType());
logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toLowerCase(), logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toString().toLowerCase(),
"Episode".equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName()); BaseItemKind.EPISODE.equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName());
if (playCommand == null) { if (playCommand == null) {
browseItem(item); browseItem(item);
} else { } else {
@ -423,6 +501,20 @@ public class JellyfinClientHandler extends BaseThingHandler {
getServerHandler().playItem(lastSessionId, playCommand, item.getId().toString(), startPositionTicks); 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 { private void browseItem(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
if (stopCurrentPlayback()) { if (stopCurrentPlayback()) {
cancelDelayedCommand(); cancelDelayedCommand();
@ -517,10 +609,11 @@ public class JellyfinClientHandler extends BaseThingHandler {
} }
private void cleanChannels() { private void cleanChannels() {
List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_NAME_CHANNEL, List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_ID_CHANNEL,
PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL, PLAYING_ITEM_SEASON_CHANNEL, PLAYING_ITEM_NAME_CHANNEL, PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL,
PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL, PLAYING_ITEM_TYPE_CHANNEL, PLAYING_ITEM_SEASON_CHANNEL, PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL,
PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL).forEach(this::cleanChannel); PLAYING_ITEM_TYPE_CHANNEL, PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL)
.forEach(this::cleanChannel);
} }
private void cleanChannel(String channelId) { private void cleanChannel(String channelId) {

View 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.AuthenticationResult;
import org.jellyfin.sdk.model.api.BaseItemDto; import org.jellyfin.sdk.model.api.BaseItemDto;
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult; 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.ItemFields;
import org.jellyfin.sdk.model.api.MessageCommand; import org.jellyfin.sdk.model.api.MessageCommand;
import org.jellyfin.sdk.model.api.PlayCommand; import org.jellyfin.sdk.model.api.PlayCommand;
@ -271,7 +272,7 @@ public class JellyfinServerHandler extends BaseBridgeHandler {
awaiter.awaitResponse(); 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 { throws SyncCallback.SyncCallbackError, ApiClientException {
var awaiter = new EmptySyncResponse(); var awaiter = new EmptySyncResponse();
new SessionApi(jellyApiClient).displayContent(sessionId, itemType, itemId, itemName, awaiter); new SessionApi(jellyApiClient).displayContent(sessionId, itemType, itemId, itemName, awaiter);
@ -287,7 +288,7 @@ public class JellyfinServerHandler extends BaseBridgeHandler {
throws SyncCallback.SyncCallbackError, ApiClientException { throws SyncCallback.SyncCallbackError, ApiClientException {
var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>(); var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>();
new TvShowsApi(jellyApiClient).getNextUp(jellyApiClient.getUserId(), null, limit, null, seriesId.toString(), 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(); var result = asyncContinuation.awaitContent();
return Objects.requireNonNull(result.getItems()); return Objects.requireNonNull(result.getItems());
} }
@ -301,7 +302,8 @@ public class JellyfinServerHandler extends BaseBridgeHandler {
throws SyncCallback.SyncCallbackError, ApiClientException { throws SyncCallback.SyncCallbackError, ApiClientException {
var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>(); var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>();
new ItemsApi(jellyApiClient).getResumeItems(Objects.requireNonNull(jellyApiClient.getUserId()), null, limit, 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(); var result = asyncContinuation.awaitContent();
return Objects.requireNonNull(result.getItems()); return Objects.requireNonNull(result.getItems());
} }
@ -320,21 +322,34 @@ public class JellyfinServerHandler extends BaseBridgeHandler {
return Objects.requireNonNull(result.getItems()); 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 { @Nullable List<ItemFields> fields) throws SyncCallback.SyncCallbackError, ApiClientException {
return searchItems(searchTerm, itemType, fields, 1).stream().findFirst().orElse(null); 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 { @Nullable List<ItemFields> fields, int limit) throws SyncCallback.SyncCallbackError, ApiClientException {
var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>(); var asyncContinuation = new SyncResponse<BaseItemDtoQueryResult>();
var itemTypes = itemType != null ? List.of(itemType) : null; var itemTypes = itemType != null ? List.of(itemType) : null;
new ItemsApi(jellyApiClient).getItems(jellyApiClient.getUserId(), null, null, null, null, null, null, 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, 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, null, limit, true, searchTerm, null, null, fields, null,
null, null, null, null, null, null, null, 1, null, null, null, null, null, null, null, null, null, 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, 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(); var response = asyncContinuation.awaitContent();
return Objects.requireNonNull(response.getItems()); return Objects.requireNonNull(response.getItems());
} }

View File

@ -29,18 +29,28 @@ thing-type.config.jellyfin.server.userId.description = The user id
# channel types # 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.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.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.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-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.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-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.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.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.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-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.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-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.label = Playing Item Name
channel-type.jellyfin.playing-item-name-channel.description = Name of the item currently playing 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 channel-type.jellyfin.playing-item-percentage-channel.label = Playing Item Percentage

View File

@ -55,6 +55,7 @@
<channels> <channels>
<channel id="send-notification" typeId="send-notification-channel"/> <channel id="send-notification" typeId="send-notification-channel"/>
<channel id="media-control" typeId="system.media-control"/> <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-name" typeId="playing-item-name-channel"/>
<channel id="playing-item-series-name" typeId="playing-item-series-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="playing-item-season-name" typeId="playing-item-season-name-channel"/>
@ -69,6 +70,10 @@
<channel id="play-next-by-terms" typeId="play-next-by-terms-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="play-last-by-terms" typeId="play-last-by-terms-channel"/>
<channel id="browse-by-terms" typeId="browse-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> </channels>
<config-description> <config-description>
@ -81,6 +86,12 @@
<label>Send Notification</label> <label>Send Notification</label>
<description>Send notification to the client</description> <description>Send notification to the client</description>
</channel-type> </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"> <channel-type id="playing-item-name-channel">
<item-type>String</item-type> <item-type>String</item-type>
<label>Playing Item Name</label> <label>Playing Item Name</label>
@ -159,4 +170,24 @@
<label>Browse By Terms</label> <label>Browse By Terms</label>
<description>Browse media by terms, works for series, episodes and movies</description> <description>Browse media by terms, works for series, episodes and movies</description>
</channel-type> </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> </thing:thing-descriptions>