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 return new UUID(new BigInteger(itemId.substring(0, 16), 16).longValue(),
247 new BigInteger(itemId.substring(16), 16).longValue());
251 public void dispose() {
253 cancelDelayedCommand();
256 private void cancelDelayedCommand() {
257 var delayedCommand = this.delayedCommand;
258 if (delayedCommand != null) {
259 delayedCommand.cancel(true);
263 private void refreshState() {
264 getServerHandler().updateClientState(this);
267 private void updateChannelStates(@Nullable BaseItemDto playingItem, @Nullable PlayerStateInfo playState) {
268 lastPlayingState = playingItem != null;
269 lastRunTimeTicks = playingItem != null ? Objects.requireNonNull(playingItem.getRunTimeTicks()) : 0L;
270 var positionTicks = playState != null ? playState.getPositionTicks() : null;
271 var runTimeTicks = playingItem != null ? playingItem.getRunTimeTicks() : null;
272 if (isLinked(MEDIA_CONTROL_CHANNEL)) {
273 updateState(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL),
274 playingItem != null && playState != null && !playState.isPaused() ? PlayPauseType.PLAY
275 : PlayPauseType.PAUSE);
277 if (isLinked(PLAYING_ITEM_PERCENTAGE_CHANNEL)) {
278 if (positionTicks != null && runTimeTicks != null) {
279 int percentage = (int) Math.round((positionTicks * 100.0) / runTimeTicks);
280 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_PERCENTAGE_CHANNEL),
281 new PercentType(percentage));
283 cleanChannel(PLAYING_ITEM_PERCENTAGE_CHANNEL);
286 if (isLinked(PLAYING_ITEM_SECOND_CHANNEL)) {
287 if (positionTicks != null) {
288 var second = Math.round((float) positionTicks / 10000000.0);
289 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SECOND_CHANNEL), new DecimalType(second));
291 cleanChannel(PLAYING_ITEM_SECOND_CHANNEL);
294 if (isLinked(PLAYING_ITEM_TOTAL_SECOND_CHANNEL)) {
295 if (runTimeTicks != null) {
296 var seconds = Math.round((float) runTimeTicks / 10000000.0);
297 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TOTAL_SECOND_CHANNEL),
298 new DecimalType(seconds));
300 cleanChannel(PLAYING_ITEM_TOTAL_SECOND_CHANNEL);
303 if (isLinked(PLAYING_ITEM_ID_CHANNEL)) {
304 if (playingItem != null) {
305 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_ID_CHANNEL),
306 new StringType(playingItem.getId().toString()));
308 cleanChannel(PLAYING_ITEM_ID_CHANNEL);
311 if (isLinked(PLAYING_ITEM_NAME_CHANNEL)) {
312 if (playingItem != null) {
313 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_NAME_CHANNEL),
314 new StringType(playingItem.getName()));
316 cleanChannel(PLAYING_ITEM_NAME_CHANNEL);
319 if (isLinked(PLAYING_ITEM_SERIES_NAME_CHANNEL)) {
320 if (playingItem != null) {
321 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SERIES_NAME_CHANNEL),
322 new StringType(playingItem.getSeriesName()));
324 cleanChannel(PLAYING_ITEM_SERIES_NAME_CHANNEL);
327 if (isLinked(PLAYING_ITEM_SEASON_NAME_CHANNEL)) {
328 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
329 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_NAME_CHANNEL),
330 new StringType(playingItem.getSeasonName()));
332 cleanChannel(PLAYING_ITEM_SEASON_NAME_CHANNEL);
335 if (isLinked(PLAYING_ITEM_SEASON_CHANNEL)) {
336 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
337 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_CHANNEL),
338 new DecimalType(Objects.requireNonNull(playingItem.getParentIndexNumber())));
340 cleanChannel(PLAYING_ITEM_SEASON_CHANNEL);
343 if (isLinked(PLAYING_ITEM_EPISODE_CHANNEL)) {
344 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
345 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_EPISODE_CHANNEL),
346 new DecimalType(Objects.requireNonNull(playingItem.getIndexNumber())));
348 cleanChannel(PLAYING_ITEM_EPISODE_CHANNEL);
351 if (isLinked(PLAYING_ITEM_GENRES_CHANNEL)) {
352 if (playingItem != null) {
353 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_GENRES_CHANNEL),
354 new StringType(String.join(",", Objects.requireNonNull(playingItem.getGenres()))));
356 cleanChannel(PLAYING_ITEM_GENRES_CHANNEL);
359 if (isLinked(PLAYING_ITEM_TYPE_CHANNEL)) {
360 if (playingItem != null) {
361 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TYPE_CHANNEL),
362 new StringType(playingItem.getType().toString()));
364 cleanChannel(PLAYING_ITEM_TYPE_CHANNEL);
369 private void runItemSearch(String terms, @Nullable PlayCommand playCommand)
370 throws SyncCallback.SyncCallbackError, ApiClientException {
371 if (terms.isBlank() || UnDefType.NULL.toFullString().equals(terms)) {
374 // detect series search with season and episode info
375 var seriesEpisodeMatcher = seriesSearchPattern.matcher(terms);
376 if (seriesEpisodeMatcher.matches()) {
377 var season = Integer.parseInt(seriesEpisodeMatcher.group("season"));
378 var episode = Integer.parseInt(seriesEpisodeMatcher.group("episode"));
379 var cleanTerms = seriesEpisodeMatcher.group("terms");
380 runSeriesEpisode(cleanTerms, season, episode, playCommand);
383 // detect search with type info or consider all types are enabled
384 var typeMatcher = typeSearchPattern.matcher(terms);
385 boolean searchByTypeEnabled = typeMatcher.matches();
386 var type = searchByTypeEnabled ? typeMatcher.group("type") : "";
387 boolean movieSearchEnabled = !searchByTypeEnabled || "movie".equals(type);
388 boolean seriesSearchEnabled = !searchByTypeEnabled || "series".equals(type);
389 boolean episodeSearchEnabled = !searchByTypeEnabled || "episode".equals(type);
390 var searchTerms = searchByTypeEnabled ? typeMatcher.group("terms") : terms;
391 runItemSearchByType(searchTerms, playCommand, movieSearchEnabled, seriesSearchEnabled, episodeSearchEnabled);
394 private void runItemSearchByType(String terms, @Nullable PlayCommand playCommand, boolean movieSearchEnabled,
395 boolean seriesSearchEnabled, boolean episodeSearchEnabled)
396 throws SyncCallback.SyncCallbackError, ApiClientException {
397 var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.SERIES, null) : null;
398 var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.MOVIE, null) : null;
399 var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.EPISODE, null)
401 if (movieItem != null) {
402 logger.debug("Found movie: '{}'", movieItem.getName());
404 if (seriesItem != null) {
405 logger.debug("Found series: '{}'", seriesItem.getName());
407 if (episodeItem != null) {
408 logger.debug("Found episode: '{}'", episodeItem.getName());
410 if (movieItem != null) {
411 runItem(movieItem, playCommand);
412 } else if (seriesItem != null) {
413 runSeriesItem(seriesItem, playCommand);
414 } else if (episodeItem != null) {
415 runItem(episodeItem, playCommand);
417 logger.warn("Nothing to display for: {}", terms);
421 private void runSeriesItem(BaseItemDto seriesItem, @Nullable PlayCommand playCommand)
422 throws SyncCallback.SyncCallbackError, ApiClientException {
423 if (playCommand != null) {
424 var resumeEpisodeItem = getServerHandler().getSeriesResumeItem(seriesItem.getId());
425 var nextUpEpisodeItem = getServerHandler().getSeriesNextUpItem(seriesItem.getId());
426 var firstEpisodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), 1, 1);
427 if (resumeEpisodeItem != null) {
428 logger.debug("Resuming series '{}' episode '{}'", seriesItem.getName(), resumeEpisodeItem.getName());
429 playItem(resumeEpisodeItem, playCommand,
430 Objects.requireNonNull(resumeEpisodeItem.getUserData()).getPlaybackPositionTicks());
431 } else if (nextUpEpisodeItem != null) {
432 logger.debug("Playing next series '{}' episode '{}'", seriesItem.getName(),
433 nextUpEpisodeItem.getName());
434 playItem(nextUpEpisodeItem, playCommand);
435 } else if (firstEpisodeItem != null) {
436 logger.debug("Playing series '{}' first episode '{}'", seriesItem.getName(),
437 firstEpisodeItem.getName());
438 playItem(firstEpisodeItem, playCommand);
440 logger.warn("Unable to found episode for series");
443 logger.debug("Browse series '{}'", seriesItem.getName());
444 browseItem(seriesItem);
448 private void runSeriesEpisode(String terms, int season, int episode, @Nullable PlayCommand playCommand)
449 throws SyncCallback.SyncCallbackError, ApiClientException {
450 logger.debug("{} series episode mode", playCommand != null ? "Play" : "Browse");
451 var seriesItem = getServerHandler().searchItem(terms, BaseItemKind.SERIES, null);
452 if (seriesItem != null) {
453 logger.debug("Searching series {} episode {}x{}", seriesItem.getName(), season, episode);
454 var episodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), season, episode);
455 if (episodeItem != null) {
456 runItem(episodeItem, playCommand);
458 logger.warn("Series {} episode {}x{} not found", seriesItem.getName(), season, episode);
461 logger.warn("Series not found");
465 private void runItem(BaseItemDto item, @Nullable PlayCommand playCommand)
466 throws SyncCallback.SyncCallbackError, ApiClientException {
467 var itemType = Objects.requireNonNull(item.getType());
468 logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toString().toLowerCase(),
469 BaseItemKind.EPISODE.equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName());
470 if (playCommand == null) {
473 playItem(item, playCommand);
477 private void playItem(BaseItemDto item, PlayCommand playCommand)
478 throws SyncCallback.SyncCallbackError, ApiClientException {
479 playItem(item, playCommand, null);
482 private void playItem(BaseItemDto item, PlayCommand playCommand, @Nullable Long startPositionTicks)
483 throws SyncCallback.SyncCallbackError, ApiClientException {
484 if (playCommand.equals(PlayCommand.PLAY_NOW) && stopCurrentPlayback()) {
485 cancelDelayedCommand();
486 delayedCommand = scheduler.schedule(() -> {
488 playItemInternal(item, playCommand, startPositionTicks);
489 } catch (SyncCallback.SyncCallbackError | ApiClientException e) {
490 logger.warn("Unexpected error while running channel {}: {}", PLAY_BY_TERMS_CHANNEL, e.getMessage());
492 }, 3, TimeUnit.SECONDS);
494 playItemInternal(item, playCommand, startPositionTicks);
498 private void playItemInternal(BaseItemDto item, PlayCommand playCommand, @Nullable Long startPositionTicks)
499 throws SyncCallback.SyncCallbackError, ApiClientException {
500 getServerHandler().playItem(lastSessionId, playCommand, item.getId().toString(), startPositionTicks);
503 private void runItemById(UUID itemId, @Nullable PlayCommand playCommand)
504 throws SyncCallback.SyncCallbackError, ApiClientException {
505 var item = getServerHandler().getItem(itemId, null);
507 logger.warn("Unable to find item with id: {}", itemId);
510 if (BaseItemKind.SERIES.equals(item.getType())) {
511 runSeriesItem(item, playCommand);
513 runItem(item, playCommand);
517 private void browseItem(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
518 if (stopCurrentPlayback()) {
519 cancelDelayedCommand();
520 delayedCommand = scheduler.schedule(() -> {
522 browseItemInternal(item);
523 } catch (SyncCallback.SyncCallbackError | ApiClientException e) {
524 logger.warn("Unexpected error while running channel {}: {}", BROWSE_ITEM_BY_TERMS_CHANNEL,
527 }, 3, TimeUnit.SECONDS);
529 browseItemInternal(item);
533 private void browseItemInternal(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
534 getServerHandler().browseToItem(lastSessionId, Objects.requireNonNull(item.getType()), item.getId().toString(),
535 Objects.requireNonNull(item.getName()));
538 private boolean stopCurrentPlayback() throws SyncCallback.SyncCallbackError, ApiClientException {
539 if (lastPlayingState) {
540 sendPlayStateCommand(PlaystateCommand.STOP);
546 private void sendPlayStateCommand(PlaystateCommand command)
547 throws SyncCallback.SyncCallbackError, ApiClientException {
548 sendPlayStateCommand(command, null);
551 private void sendPlayStateCommand(PlaystateCommand command, @Nullable Long seekPositionTick)
552 throws SyncCallback.SyncCallbackError, ApiClientException {
553 getServerHandler().sendPlayStateCommand(lastSessionId, command, seekPositionTick);
556 private void sendDeviceMessage(Command command) throws SyncCallback.SyncCallbackError, ApiClientException {
557 getServerHandler().sendDeviceMessage(lastSessionId, "Jellyfin OpenHAB", command.toFullString(), 15000);
560 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
561 throws SyncCallback.SyncCallbackError, ApiClientException {
562 if (command instanceof RefreshType) {
564 } else if (command instanceof PlayPauseType) {
565 if (command == PlayPauseType.PLAY) {
566 sendPlayStateCommand(PlaystateCommand.UNPAUSE);
567 updateState(channelUID, PlayPauseType.PLAY);
568 } else if (command == PlayPauseType.PAUSE) {
569 sendPlayStateCommand(PlaystateCommand.PAUSE);
570 updateState(channelUID, PlayPauseType.PAUSE);
572 } else if (command instanceof NextPreviousType) {
573 if (command == NextPreviousType.NEXT) {
574 sendPlayStateCommand(PlaystateCommand.NEXT_TRACK);
575 } else if (command == NextPreviousType.PREVIOUS) {
576 sendPlayStateCommand(PlaystateCommand.PREVIOUS_TRACK);
578 } else if (command instanceof RewindFastforwardType) {
579 if (command == RewindFastforwardType.FASTFORWARD) {
580 sendPlayStateCommand(PlaystateCommand.FAST_FORWARD);
581 } else if (command == RewindFastforwardType.REWIND) {
582 sendPlayStateCommand(PlaystateCommand.REWIND);
585 logger.warn("Unknown media control command: {}", command);
589 private void seekToPercentage(int percentage) throws SyncCallback.SyncCallbackError, ApiClientException {
590 if (lastRunTimeTicks == 0L) {
591 logger.warn("Can't seek missing RunTimeTicks info");
594 var seekPositionTick = Math.round(((float) lastRunTimeTicks) * ((float) percentage / 100.0));
595 logger.debug("Seek to {}%: {} of {}", percentage, seekPositionTick, lastRunTimeTicks);
596 seekToTick(seekPositionTick);
599 private void seekToSecond(long second) throws SyncCallback.SyncCallbackError, ApiClientException {
600 long seekPositionTick = second * 10000000L;
601 logger.debug("Seek to second {}: {} of {}", second, seekPositionTick, lastRunTimeTicks);
602 seekToTick(seekPositionTick);
605 private void seekToTick(long seekPositionTick) throws SyncCallback.SyncCallbackError, ApiClientException {
606 sendPlayStateCommand(PlaystateCommand.SEEK, seekPositionTick);
607 scheduler.schedule(this::refreshState, 3, TimeUnit.SECONDS);
610 private void cleanChannels() {
611 List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_ID_CHANNEL,
612 PLAYING_ITEM_NAME_CHANNEL, PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL,
613 PLAYING_ITEM_SEASON_CHANNEL, PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL,
614 PLAYING_ITEM_TYPE_CHANNEL, PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL)
615 .forEach(this::cleanChannel);
618 private void cleanChannel(String channelId) {
619 updateState(new ChannelUID(this.thing.getUID(), channelId), UnDefType.NULL);
622 private JellyfinServerHandler getServerHandler() {
623 var bridge = Objects.requireNonNull(getBridge());
624 return (JellyfinServerHandler) Objects.requireNonNull(bridge.getHandler());