2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.jellyfin.internal.handler;
15 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.BROWSE_ITEM_BY_ID_CHANNEL;
16 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.BROWSE_ITEM_BY_TERMS_CHANNEL;
17 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.MEDIA_CONTROL_CHANNEL;
18 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_EPISODE_CHANNEL;
19 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_GENRES_CHANNEL;
20 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_ID_CHANNEL;
21 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_NAME_CHANNEL;
22 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_PERCENTAGE_CHANNEL;
23 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_SEASON_CHANNEL;
24 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_SEASON_NAME_CHANNEL;
25 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_SECOND_CHANNEL;
26 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_SERIES_NAME_CHANNEL;
27 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_TOTAL_SECOND_CHANNEL;
28 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAYING_ITEM_TYPE_CHANNEL;
29 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_BY_ID_CHANNEL;
30 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_BY_TERMS_CHANNEL;
31 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_LAST_BY_ID_CHANNEL;
32 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_LAST_BY_TERMS_CHANNEL;
33 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_NEXT_BY_ID_CHANNEL;
34 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.PLAY_NEXT_BY_TERMS_CHANNEL;
35 import static org.openhab.binding.jellyfin.internal.JellyfinBindingConstants.SEND_NOTIFICATION_CHANNEL;
37 import java.math.BigInteger;
38 import java.util.List;
39 import java.util.Objects;
40 import java.util.UUID;
41 import java.util.concurrent.ScheduledFuture;
42 import java.util.concurrent.TimeUnit;
43 import java.util.regex.Pattern;
45 import org.eclipse.jdt.annotation.NonNullByDefault;
46 import org.eclipse.jdt.annotation.Nullable;
47 import org.jellyfin.sdk.api.client.exception.ApiClientException;
48 import org.jellyfin.sdk.model.api.BaseItemDto;
49 import org.jellyfin.sdk.model.api.BaseItemKind;
50 import org.jellyfin.sdk.model.api.PlayCommand;
51 import org.jellyfin.sdk.model.api.PlayerStateInfo;
52 import org.jellyfin.sdk.model.api.PlaystateCommand;
53 import org.jellyfin.sdk.model.api.SessionInfo;
54 import org.openhab.binding.jellyfin.internal.util.SyncCallback;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.NextPreviousType;
57 import org.openhab.core.library.types.PercentType;
58 import org.openhab.core.library.types.PlayPauseType;
59 import org.openhab.core.library.types.RewindFastforwardType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.Thing;
63 import org.openhab.core.thing.ThingStatus;
64 import org.openhab.core.thing.binding.BaseThingHandler;
65 import org.openhab.core.types.Command;
66 import org.openhab.core.types.RefreshType;
67 import org.openhab.core.types.UnDefType;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
72 * The {@link JellyfinClientHandler} is responsible for handling commands, which are
73 * sent to one of the channels.
75 * @author Miguel Álvarez - Initial contribution
78 public class JellyfinClientHandler extends BaseThingHandler {
80 private final Logger logger = LoggerFactory.getLogger(JellyfinClientHandler.class);
81 private final Pattern typeSearchPattern = Pattern.compile("<type:(?<type>movie|series|episode)>\\s?(?<terms>.*)");
82 private final Pattern seriesSearchPattern = Pattern
83 .compile("(<type:series>)?<season:(?<season>[0-9]*)><episode:(?<episode>[0-9]*)>\\s?(?<terms>.*)");
84 private @Nullable ScheduledFuture<?> delayedCommand;
85 private String lastSessionId = "";
86 private boolean lastPlayingState = false;
87 private long lastRunTimeTicks = 0L;
89 public JellyfinClientHandler(Thing thing) {
94 public void initialize() {
95 updateStatus(ThingStatus.UNKNOWN);
96 scheduler.execute(() -> refreshState());
99 public synchronized void updateStateFromSession(@Nullable SessionInfo session) {
100 if (session != null) {
101 lastSessionId = Objects.requireNonNull(session.getId());
102 updateStatus(ThingStatus.ONLINE);
103 updateChannelStates(session.getNowPlayingItem(), session.getPlayState());
105 lastPlayingState = false;
107 updateStatus(ThingStatus.OFFLINE);
112 public void handleCommand(ChannelUID channelUID, Command command) {
114 switch (channelUID.getId()) {
115 case SEND_NOTIFICATION_CHANNEL:
116 if (command instanceof RefreshType) {
119 sendDeviceMessage(command);
121 case MEDIA_CONTROL_CHANNEL:
122 if (command instanceof RefreshType) {
126 handleMediaControlCommand(channelUID, command);
128 case PLAY_BY_TERMS_CHANNEL:
129 if (command instanceof RefreshType) {
132 runItemSearch(command.toFullString(), PlayCommand.PLAY_NOW);
134 case PLAY_NEXT_BY_TERMS_CHANNEL:
135 if (command instanceof RefreshType) {
138 runItemSearch(command.toFullString(), PlayCommand.PLAY_NEXT);
140 case PLAY_LAST_BY_TERMS_CHANNEL:
141 if (command instanceof RefreshType) {
144 runItemSearch(command.toFullString(), PlayCommand.PLAY_LAST);
146 case BROWSE_ITEM_BY_TERMS_CHANNEL:
147 if (command instanceof RefreshType) {
150 runItemSearch(command.toFullString(), null);
152 case PLAY_BY_ID_CHANNEL:
153 if (command instanceof RefreshType) {
158 itemUUID = parseItemUUID(command);
159 } catch (NumberFormatException e) {
160 logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command);
163 runItemById(itemUUID, PlayCommand.PLAY_NOW);
165 case PLAY_NEXT_BY_ID_CHANNEL:
166 if (command instanceof RefreshType) {
170 itemUUID = parseItemUUID(command);
171 } catch (NumberFormatException e) {
172 logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command);
175 runItemById(itemUUID, PlayCommand.PLAY_NEXT);
177 case PLAY_LAST_BY_ID_CHANNEL:
178 if (command instanceof RefreshType) {
182 itemUUID = parseItemUUID(command);
183 } catch (NumberFormatException e) {
184 logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command);
187 runItemById(itemUUID, PlayCommand.PLAY_LAST);
189 case BROWSE_ITEM_BY_ID_CHANNEL:
190 if (command instanceof RefreshType) {
194 itemUUID = parseItemUUID(command);
195 } catch (NumberFormatException e) {
196 logger.warn("Thing {}: Unable to parse item UUID in command {}.", thing.getUID(), command);
199 runItemById(itemUUID, null);
201 case PLAYING_ITEM_SECOND_CHANNEL:
202 if (command instanceof RefreshType) {
206 if (command.toFullString().equals(UnDefType.NULL.toFullString())) {
209 seekToSecond(Long.parseLong(command.toFullString()));
211 case PLAYING_ITEM_PERCENTAGE_CHANNEL:
212 if (command instanceof RefreshType) {
216 if (command.toFullString().equals(UnDefType.NULL.toFullString())) {
219 seekToPercentage(Integer.parseInt(command.toFullString()));
221 case PLAYING_ITEM_ID_CHANNEL:
222 case PLAYING_ITEM_NAME_CHANNEL:
223 case PLAYING_ITEM_GENRES_CHANNEL:
224 case PLAYING_ITEM_SEASON_CHANNEL:
225 case PLAYING_ITEM_EPISODE_CHANNEL:
226 case PLAYING_ITEM_SERIES_NAME_CHANNEL:
227 case PLAYING_ITEM_SEASON_NAME_CHANNEL:
228 case PLAYING_ITEM_TYPE_CHANNEL:
229 case PLAYING_ITEM_TOTAL_SECOND_CHANNEL:
230 if (command instanceof RefreshType) {
236 } catch (SyncCallback.SyncCallbackError syncCallbackError) {
237 logger.warn("Unexpected error while running channel {}: {}", channelUID.getId(),
238 syncCallbackError.getMessage());
239 } catch (ApiClientException e) {
240 getServerHandler().handleApiException(e);
244 private UUID parseItemUUID(Command command) throws NumberFormatException {
245 var itemId = command.toFullString().replace("-", "");
246 UUID itemUUID = new UUID(new BigInteger(itemId.substring(0, 16), 16).longValue(),
247 new BigInteger(itemId.substring(16), 16).longValue());
252 public void dispose() {
254 cancelDelayedCommand();
257 private void cancelDelayedCommand() {
258 var delayedCommand = this.delayedCommand;
259 if (delayedCommand != null) {
260 delayedCommand.cancel(true);
264 private void refreshState() {
265 getServerHandler().updateClientState(this);
268 private void updateChannelStates(@Nullable BaseItemDto playingItem, @Nullable PlayerStateInfo playState) {
269 lastPlayingState = playingItem != null;
270 lastRunTimeTicks = playingItem != null ? Objects.requireNonNull(playingItem.getRunTimeTicks()) : 0L;
271 var positionTicks = playState != null ? playState.getPositionTicks() : null;
272 var runTimeTicks = playingItem != null ? playingItem.getRunTimeTicks() : null;
273 if (isLinked(MEDIA_CONTROL_CHANNEL)) {
274 updateState(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL),
275 playingItem != null && playState != null && !playState.isPaused() ? PlayPauseType.PLAY
276 : PlayPauseType.PAUSE);
278 if (isLinked(PLAYING_ITEM_PERCENTAGE_CHANNEL)) {
279 if (positionTicks != null && runTimeTicks != null) {
280 int percentage = (int) Math.round((positionTicks * 100.0) / runTimeTicks);
281 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_PERCENTAGE_CHANNEL),
282 new PercentType(percentage));
284 cleanChannel(PLAYING_ITEM_PERCENTAGE_CHANNEL);
287 if (isLinked(PLAYING_ITEM_SECOND_CHANNEL)) {
288 if (positionTicks != null) {
289 var second = Math.round((float) positionTicks / 10000000.0);
290 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SECOND_CHANNEL), new DecimalType(second));
292 cleanChannel(PLAYING_ITEM_SECOND_CHANNEL);
295 if (isLinked(PLAYING_ITEM_TOTAL_SECOND_CHANNEL)) {
296 if (runTimeTicks != null) {
297 var seconds = Math.round((float) runTimeTicks / 10000000.0);
298 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TOTAL_SECOND_CHANNEL),
299 new DecimalType(seconds));
301 cleanChannel(PLAYING_ITEM_TOTAL_SECOND_CHANNEL);
304 if (isLinked(PLAYING_ITEM_ID_CHANNEL)) {
305 if (playingItem != null) {
306 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_ID_CHANNEL),
307 new StringType(playingItem.getId().toString()));
309 cleanChannel(PLAYING_ITEM_ID_CHANNEL);
312 if (isLinked(PLAYING_ITEM_NAME_CHANNEL)) {
313 if (playingItem != null) {
314 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_NAME_CHANNEL),
315 new StringType(playingItem.getName()));
317 cleanChannel(PLAYING_ITEM_NAME_CHANNEL);
320 if (isLinked(PLAYING_ITEM_SERIES_NAME_CHANNEL)) {
321 if (playingItem != null) {
322 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SERIES_NAME_CHANNEL),
323 new StringType(playingItem.getSeriesName()));
325 cleanChannel(PLAYING_ITEM_SERIES_NAME_CHANNEL);
328 if (isLinked(PLAYING_ITEM_SEASON_NAME_CHANNEL)) {
329 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
330 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_NAME_CHANNEL),
331 new StringType(playingItem.getSeasonName()));
333 cleanChannel(PLAYING_ITEM_SEASON_NAME_CHANNEL);
336 if (isLinked(PLAYING_ITEM_SEASON_CHANNEL)) {
337 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
338 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_CHANNEL),
339 new DecimalType(Objects.requireNonNull(playingItem.getParentIndexNumber())));
341 cleanChannel(PLAYING_ITEM_SEASON_CHANNEL);
344 if (isLinked(PLAYING_ITEM_EPISODE_CHANNEL)) {
345 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
346 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_EPISODE_CHANNEL),
347 new DecimalType(Objects.requireNonNull(playingItem.getIndexNumber())));
349 cleanChannel(PLAYING_ITEM_EPISODE_CHANNEL);
352 if (isLinked(PLAYING_ITEM_GENRES_CHANNEL)) {
353 if (playingItem != null) {
354 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_GENRES_CHANNEL),
355 new StringType(String.join(",", Objects.requireNonNull(playingItem.getGenres()))));
357 cleanChannel(PLAYING_ITEM_GENRES_CHANNEL);
360 if (isLinked(PLAYING_ITEM_TYPE_CHANNEL)) {
361 if (playingItem != null) {
362 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TYPE_CHANNEL),
363 new StringType(playingItem.getType().toString()));
365 cleanChannel(PLAYING_ITEM_TYPE_CHANNEL);
370 private void runItemSearch(String terms, @Nullable PlayCommand playCommand)
371 throws SyncCallback.SyncCallbackError, ApiClientException {
372 if (terms.isBlank() || UnDefType.NULL.toFullString().equals(terms)) {
375 // detect series search with season and episode info
376 var seriesEpisodeMatcher = seriesSearchPattern.matcher(terms);
377 if (seriesEpisodeMatcher.matches()) {
378 var season = Integer.parseInt(seriesEpisodeMatcher.group("season"));
379 var episode = Integer.parseInt(seriesEpisodeMatcher.group("episode"));
380 var cleanTerms = seriesEpisodeMatcher.group("terms");
381 runSeriesEpisode(cleanTerms, season, episode, playCommand);
384 // detect search with type info or consider all types are enabled
385 var typeMatcher = typeSearchPattern.matcher(terms);
386 boolean searchByTypeEnabled = typeMatcher.matches();
387 var type = searchByTypeEnabled ? typeMatcher.group("type") : "";
388 boolean movieSearchEnabled = !searchByTypeEnabled || type.equals("movie");
389 boolean seriesSearchEnabled = !searchByTypeEnabled || type.equals("series");
390 boolean episodeSearchEnabled = !searchByTypeEnabled || type.equals("episode");
391 var searchTerms = searchByTypeEnabled ? typeMatcher.group("terms") : terms;
392 runItemSearchByType(searchTerms, playCommand, movieSearchEnabled, seriesSearchEnabled, episodeSearchEnabled);
395 private void runItemSearchByType(String terms, @Nullable PlayCommand playCommand, boolean movieSearchEnabled,
396 boolean seriesSearchEnabled, boolean episodeSearchEnabled)
397 throws SyncCallback.SyncCallbackError, ApiClientException {
398 var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.SERIES, null) : null;
399 var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.MOVIE, null) : null;
400 var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.EPISODE, null)
402 if (movieItem != null) {
403 logger.debug("Found movie: '{}'", movieItem.getName());
405 if (seriesItem != null) {
406 logger.debug("Found series: '{}'", seriesItem.getName());
408 if (episodeItem != null) {
409 logger.debug("Found episode: '{}'", episodeItem.getName());
411 if (movieItem != null) {
412 runItem(movieItem, playCommand);
413 } else if (seriesItem != null) {
414 runSeriesItem(seriesItem, playCommand);
415 } else if (episodeItem != null) {
416 runItem(episodeItem, playCommand);
418 logger.warn("Nothing to display for: {}", terms);
422 private void runSeriesItem(BaseItemDto seriesItem, @Nullable PlayCommand playCommand)
423 throws SyncCallback.SyncCallbackError, ApiClientException {
424 if (playCommand != null) {
425 var resumeEpisodeItem = getServerHandler().getSeriesResumeItem(seriesItem.getId());
426 var nextUpEpisodeItem = getServerHandler().getSeriesNextUpItem(seriesItem.getId());
427 var firstEpisodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), 1, 1);
428 if (resumeEpisodeItem != null) {
429 logger.debug("Resuming series '{}' episode '{}'", seriesItem.getName(), resumeEpisodeItem.getName());
430 playItem(resumeEpisodeItem, playCommand,
431 Objects.requireNonNull(resumeEpisodeItem.getUserData()).getPlaybackPositionTicks());
432 } else if (nextUpEpisodeItem != null) {
433 logger.debug("Playing next series '{}' episode '{}'", seriesItem.getName(),
434 nextUpEpisodeItem.getName());
435 playItem(nextUpEpisodeItem, playCommand);
436 } else if (firstEpisodeItem != null) {
437 logger.debug("Playing series '{}' first episode '{}'", seriesItem.getName(),
438 firstEpisodeItem.getName());
439 playItem(firstEpisodeItem, playCommand);
441 logger.warn("Unable to found episode for series");
444 logger.debug("Browse series '{}'", seriesItem.getName());
445 browseItem(seriesItem);
449 private void runSeriesEpisode(String terms, int season, int episode, @Nullable PlayCommand playCommand)
450 throws SyncCallback.SyncCallbackError, ApiClientException {
451 logger.debug("{} series episode mode", playCommand != null ? "Play" : "Browse");
452 var seriesItem = getServerHandler().searchItem(terms, BaseItemKind.SERIES, null);
453 if (seriesItem != null) {
454 logger.debug("Searching series {} episode {}x{}", seriesItem.getName(), season, episode);
455 var episodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), season, episode);
456 if (episodeItem != null) {
457 runItem(episodeItem, playCommand);
459 logger.warn("Series {} episode {}x{} not found", seriesItem.getName(), season, episode);
462 logger.warn("Series not found");
466 private void runItem(BaseItemDto item, @Nullable PlayCommand playCommand)
467 throws SyncCallback.SyncCallbackError, ApiClientException {
468 var itemType = Objects.requireNonNull(item.getType());
469 logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toString().toLowerCase(),
470 BaseItemKind.EPISODE.equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName());
471 if (playCommand == null) {
474 playItem(item, playCommand);
478 private void playItem(BaseItemDto item, PlayCommand playCommand)
479 throws SyncCallback.SyncCallbackError, ApiClientException {
480 playItem(item, playCommand, null);
483 private void playItem(BaseItemDto item, PlayCommand playCommand, @Nullable Long startPositionTicks)
484 throws SyncCallback.SyncCallbackError, ApiClientException {
485 if (playCommand.equals(PlayCommand.PLAY_NOW) && stopCurrentPlayback()) {
486 cancelDelayedCommand();
487 delayedCommand = scheduler.schedule(() -> {
489 playItemInternal(item, playCommand, startPositionTicks);
490 } catch (SyncCallback.SyncCallbackError | ApiClientException e) {
491 logger.warn("Unexpected error while running channel {}: {}", PLAY_BY_TERMS_CHANNEL, e.getMessage());
493 }, 3, TimeUnit.SECONDS);
495 playItemInternal(item, playCommand, startPositionTicks);
499 private void playItemInternal(BaseItemDto item, PlayCommand playCommand, @Nullable Long startPositionTicks)
500 throws SyncCallback.SyncCallbackError, ApiClientException {
501 getServerHandler().playItem(lastSessionId, playCommand, item.getId().toString(), startPositionTicks);
504 private void runItemById(UUID itemId, @Nullable PlayCommand playCommand)
505 throws SyncCallback.SyncCallbackError, ApiClientException {
506 var item = getServerHandler().getItem(itemId, null);
508 logger.warn("Unable to find item with id: {}", itemId);
511 if (BaseItemKind.SERIES.equals(item.getType())) {
512 runSeriesItem(item, playCommand);
514 runItem(item, playCommand);
518 private void browseItem(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
519 if (stopCurrentPlayback()) {
520 cancelDelayedCommand();
521 delayedCommand = scheduler.schedule(() -> {
523 browseItemInternal(item);
524 } catch (SyncCallback.SyncCallbackError | ApiClientException e) {
525 logger.warn("Unexpected error while running channel {}: {}", BROWSE_ITEM_BY_TERMS_CHANNEL,
528 }, 3, TimeUnit.SECONDS);
530 browseItemInternal(item);
534 private void browseItemInternal(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
535 getServerHandler().browseToItem(lastSessionId, Objects.requireNonNull(item.getType()), item.getId().toString(),
536 Objects.requireNonNull(item.getName()));
539 private boolean stopCurrentPlayback() throws SyncCallback.SyncCallbackError, ApiClientException {
540 if (lastPlayingState) {
541 sendPlayStateCommand(PlaystateCommand.STOP);
547 private void sendPlayStateCommand(PlaystateCommand command)
548 throws SyncCallback.SyncCallbackError, ApiClientException {
549 sendPlayStateCommand(command, null);
552 private void sendPlayStateCommand(PlaystateCommand command, @Nullable Long seekPositionTick)
553 throws SyncCallback.SyncCallbackError, ApiClientException {
554 getServerHandler().sendPlayStateCommand(lastSessionId, command, seekPositionTick);
557 private void sendDeviceMessage(Command command) throws SyncCallback.SyncCallbackError, ApiClientException {
558 getServerHandler().sendDeviceMessage(lastSessionId, "Jellyfin OpenHAB", command.toFullString(), 15000);
561 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
562 throws SyncCallback.SyncCallbackError, ApiClientException {
563 if (command instanceof RefreshType) {
565 } else if (command instanceof PlayPauseType) {
566 if (command == PlayPauseType.PLAY) {
567 sendPlayStateCommand(PlaystateCommand.UNPAUSE);
568 updateState(channelUID, PlayPauseType.PLAY);
569 } else if (command == PlayPauseType.PAUSE) {
570 sendPlayStateCommand(PlaystateCommand.PAUSE);
571 updateState(channelUID, PlayPauseType.PAUSE);
573 } else if (command instanceof NextPreviousType) {
574 if (command == NextPreviousType.NEXT) {
575 sendPlayStateCommand(PlaystateCommand.NEXT_TRACK);
576 } else if (command == NextPreviousType.PREVIOUS) {
577 sendPlayStateCommand(PlaystateCommand.PREVIOUS_TRACK);
579 } else if (command instanceof RewindFastforwardType) {
580 if (command == RewindFastforwardType.FASTFORWARD) {
581 sendPlayStateCommand(PlaystateCommand.FAST_FORWARD);
582 } else if (command == RewindFastforwardType.REWIND) {
583 sendPlayStateCommand(PlaystateCommand.REWIND);
586 logger.warn("Unknown media control command: {}", command);
590 private void seekToPercentage(int percentage) throws SyncCallback.SyncCallbackError, ApiClientException {
591 if (lastRunTimeTicks == 0L) {
592 logger.warn("Can't seek missing RunTimeTicks info");
595 var seekPositionTick = Math.round(((float) lastRunTimeTicks) * ((float) percentage / 100.0));
596 logger.debug("Seek to {}%: {} of {}", percentage, seekPositionTick, lastRunTimeTicks);
597 seekToTick(seekPositionTick);
600 private void seekToSecond(long second) throws SyncCallback.SyncCallbackError, ApiClientException {
601 long seekPositionTick = second * 10000000L;
602 logger.debug("Seek to second {}: {} of {}", second, seekPositionTick, lastRunTimeTicks);
603 seekToTick(seekPositionTick);
606 private void seekToTick(long seekPositionTick) throws SyncCallback.SyncCallbackError, ApiClientException {
607 sendPlayStateCommand(PlaystateCommand.SEEK, seekPositionTick);
608 scheduler.schedule(this::refreshState, 3, TimeUnit.SECONDS);
611 private void cleanChannels() {
612 List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_ID_CHANNEL,
613 PLAYING_ITEM_NAME_CHANNEL, PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL,
614 PLAYING_ITEM_SEASON_CHANNEL, PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL,
615 PLAYING_ITEM_TYPE_CHANNEL, PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL)
616 .forEach(this::cleanChannel);
619 private void cleanChannel(String channelId) {
620 updateState(new ChannelUID(this.thing.getUID(), channelId), UnDefType.NULL);
623 private JellyfinServerHandler getServerHandler() {
624 var bridge = Objects.requireNonNull(getBridge());
625 return (JellyfinServerHandler) Objects.requireNonNull(bridge.getHandler());