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(this::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 if (command instanceof RefreshType) {
118 switch (channelUID.getId()) {
119 case SEND_NOTIFICATION_CHANNEL -> sendDeviceMessage(command);
120 case MEDIA_CONTROL_CHANNEL -> handleMediaControlCommand(channelUID, command);
121 case PLAY_BY_TERMS_CHANNEL -> runItemSearch(command.toFullString(), PlayCommand.PLAY_NOW);
122 case PLAY_NEXT_BY_TERMS_CHANNEL -> runItemSearch(command.toFullString(), PlayCommand.PLAY_NEXT);
123 case PLAY_LAST_BY_TERMS_CHANNEL -> runItemSearch(command.toFullString(), PlayCommand.PLAY_LAST);
124 case BROWSE_ITEM_BY_TERMS_CHANNEL -> runItemSearch(command.toFullString(), null);
125 case PLAY_BY_ID_CHANNEL -> runItemById(parseItemUUID(command), PlayCommand.PLAY_NOW);
126 case PLAY_NEXT_BY_ID_CHANNEL -> runItemById(parseItemUUID(command), PlayCommand.PLAY_NEXT);
127 case PLAY_LAST_BY_ID_CHANNEL -> runItemById(parseItemUUID(command), PlayCommand.PLAY_LAST);
128 case BROWSE_ITEM_BY_ID_CHANNEL -> runItemById(parseItemUUID(command), null);
129 case PLAYING_ITEM_SECOND_CHANNEL -> seekToSecond(command);
130 case PLAYING_ITEM_PERCENTAGE_CHANNEL -> seekToPercentage(command);
132 } catch (NumberFormatException numberFormatException) {
133 logger.warn("NumberFormatException error while running channel {}: {}", channelUID.getId(),
134 numberFormatException.getMessage());
135 } catch (IllegalArgumentException illegalArgumentException) {
136 logger.warn("IllegalArgumentException error while running channel {}: {}", channelUID.getId(),
137 illegalArgumentException.getMessage());
138 } catch (SyncCallback.SyncCallbackError syncCallbackError) {
139 logger.warn("Unexpected error while running channel {}: {}", channelUID.getId(),
140 syncCallbackError.getMessage());
141 } catch (ApiClientException e) {
142 getServerHandler().handleApiException(e);
146 private UUID parseItemUUID(Command command) throws IllegalArgumentException {
148 var itemId = command.toFullString().replace("-", "");
149 return new UUID(new BigInteger(itemId.substring(0, 16), 16).longValue(),
150 new BigInteger(itemId.substring(16), 16).longValue());
151 } catch (NumberFormatException ignored) {
152 throw new IllegalArgumentException("Unable to parse item UUID in command " + command.toFullString() + ".");
157 public void dispose() {
159 cancelDelayedCommand();
162 private void cancelDelayedCommand() {
163 var delayedCommand = this.delayedCommand;
164 if (delayedCommand != null) {
165 delayedCommand.cancel(true);
169 private void refreshState() {
170 getServerHandler().updateClientState(this);
173 private void updateChannelStates(@Nullable BaseItemDto playingItem, @Nullable PlayerStateInfo playState) {
174 lastPlayingState = playingItem != null;
175 lastRunTimeTicks = playingItem != null ? Objects.requireNonNull(playingItem.getRunTimeTicks()) : 0L;
176 var positionTicks = playState != null ? playState.getPositionTicks() : null;
177 var runTimeTicks = playingItem != null ? playingItem.getRunTimeTicks() : null;
178 if (isLinked(MEDIA_CONTROL_CHANNEL)) {
179 updateState(new ChannelUID(this.thing.getUID(), MEDIA_CONTROL_CHANNEL),
180 playingItem != null && playState != null && !playState.isPaused() ? PlayPauseType.PLAY
181 : PlayPauseType.PAUSE);
183 if (isLinked(PLAYING_ITEM_PERCENTAGE_CHANNEL)) {
184 if (positionTicks != null && runTimeTicks != null) {
185 int percentage = (int) Math.round((positionTicks * 100.0) / runTimeTicks);
186 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_PERCENTAGE_CHANNEL),
187 new PercentType(percentage));
189 cleanChannel(PLAYING_ITEM_PERCENTAGE_CHANNEL);
192 if (isLinked(PLAYING_ITEM_SECOND_CHANNEL)) {
193 if (positionTicks != null) {
194 var second = Math.round((float) positionTicks / 10000000.0);
195 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SECOND_CHANNEL), new DecimalType(second));
197 cleanChannel(PLAYING_ITEM_SECOND_CHANNEL);
200 if (isLinked(PLAYING_ITEM_TOTAL_SECOND_CHANNEL)) {
201 if (runTimeTicks != null) {
202 var seconds = Math.round((float) runTimeTicks / 10000000.0);
203 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TOTAL_SECOND_CHANNEL),
204 new DecimalType(seconds));
206 cleanChannel(PLAYING_ITEM_TOTAL_SECOND_CHANNEL);
209 if (isLinked(PLAYING_ITEM_ID_CHANNEL)) {
210 if (playingItem != null) {
211 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_ID_CHANNEL),
212 new StringType(playingItem.getId().toString()));
214 cleanChannel(PLAYING_ITEM_ID_CHANNEL);
217 if (isLinked(PLAYING_ITEM_NAME_CHANNEL)) {
218 if (playingItem != null) {
219 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_NAME_CHANNEL),
220 new StringType(playingItem.getName()));
222 cleanChannel(PLAYING_ITEM_NAME_CHANNEL);
225 if (isLinked(PLAYING_ITEM_SERIES_NAME_CHANNEL)) {
226 if (playingItem != null) {
227 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SERIES_NAME_CHANNEL),
228 new StringType(playingItem.getSeriesName()));
230 cleanChannel(PLAYING_ITEM_SERIES_NAME_CHANNEL);
233 if (isLinked(PLAYING_ITEM_SEASON_NAME_CHANNEL)) {
234 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
235 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_NAME_CHANNEL),
236 new StringType(playingItem.getSeasonName()));
238 cleanChannel(PLAYING_ITEM_SEASON_NAME_CHANNEL);
241 if (isLinked(PLAYING_ITEM_SEASON_CHANNEL)) {
242 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
243 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_SEASON_CHANNEL),
244 new DecimalType(Objects.requireNonNull(playingItem.getParentIndexNumber())));
246 cleanChannel(PLAYING_ITEM_SEASON_CHANNEL);
249 if (isLinked(PLAYING_ITEM_EPISODE_CHANNEL)) {
250 if (playingItem != null && BaseItemKind.EPISODE.equals(playingItem.getType())) {
251 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_EPISODE_CHANNEL),
252 new DecimalType(Objects.requireNonNull(playingItem.getIndexNumber())));
254 cleanChannel(PLAYING_ITEM_EPISODE_CHANNEL);
257 if (isLinked(PLAYING_ITEM_GENRES_CHANNEL)) {
258 if (playingItem != null) {
259 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_GENRES_CHANNEL),
260 new StringType(String.join(",", Objects.requireNonNull(playingItem.getGenres()))));
262 cleanChannel(PLAYING_ITEM_GENRES_CHANNEL);
265 if (isLinked(PLAYING_ITEM_TYPE_CHANNEL)) {
266 if (playingItem != null) {
267 updateState(new ChannelUID(this.thing.getUID(), PLAYING_ITEM_TYPE_CHANNEL),
268 new StringType(playingItem.getType().toString()));
270 cleanChannel(PLAYING_ITEM_TYPE_CHANNEL);
275 private void runItemSearch(String terms, @Nullable PlayCommand playCommand)
276 throws SyncCallback.SyncCallbackError, ApiClientException {
277 if (terms.isBlank() || UnDefType.NULL.toFullString().equals(terms)) {
280 // detect series search with season and episode info
281 var seriesEpisodeMatcher = seriesSearchPattern.matcher(terms);
282 if (seriesEpisodeMatcher.matches()) {
283 var season = Integer.parseInt(seriesEpisodeMatcher.group("season"));
284 var episode = Integer.parseInt(seriesEpisodeMatcher.group("episode"));
285 var cleanTerms = seriesEpisodeMatcher.group("terms");
286 runSeriesEpisode(cleanTerms, season, episode, playCommand);
289 // detect search with type info or consider all types are enabled
290 var typeMatcher = typeSearchPattern.matcher(terms);
291 boolean searchByTypeEnabled = typeMatcher.matches();
292 var type = searchByTypeEnabled ? typeMatcher.group("type") : "";
293 boolean movieSearchEnabled = !searchByTypeEnabled || "movie".equals(type);
294 boolean seriesSearchEnabled = !searchByTypeEnabled || "series".equals(type);
295 boolean episodeSearchEnabled = !searchByTypeEnabled || "episode".equals(type);
296 var searchTerms = searchByTypeEnabled ? typeMatcher.group("terms") : terms;
297 runItemSearchByType(searchTerms, playCommand, movieSearchEnabled, seriesSearchEnabled, episodeSearchEnabled);
300 private void runItemSearchByType(String terms, @Nullable PlayCommand playCommand, boolean movieSearchEnabled,
301 boolean seriesSearchEnabled, boolean episodeSearchEnabled)
302 throws SyncCallback.SyncCallbackError, ApiClientException {
303 var seriesItem = seriesSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.SERIES, null) : null;
304 var movieItem = movieSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.MOVIE, null) : null;
305 var episodeItem = episodeSearchEnabled ? getServerHandler().searchItem(terms, BaseItemKind.EPISODE, null)
307 if (movieItem != null) {
308 logger.debug("Found movie: '{}'", movieItem.getName());
310 if (seriesItem != null) {
311 logger.debug("Found series: '{}'", seriesItem.getName());
313 if (episodeItem != null) {
314 logger.debug("Found episode: '{}'", episodeItem.getName());
316 if (movieItem != null) {
317 runItem(movieItem, playCommand);
318 } else if (seriesItem != null) {
319 runSeriesItem(seriesItem, playCommand);
320 } else if (episodeItem != null) {
321 runItem(episodeItem, playCommand);
323 logger.warn("Nothing to display for: {}", terms);
327 private void runSeriesItem(BaseItemDto seriesItem, @Nullable PlayCommand playCommand)
328 throws SyncCallback.SyncCallbackError, ApiClientException {
329 if (playCommand != null) {
330 var resumeEpisodeItem = getServerHandler().getSeriesResumeItem(seriesItem.getId());
331 var nextUpEpisodeItem = getServerHandler().getSeriesNextUpItem(seriesItem.getId());
332 var firstEpisodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), 1, 1);
333 if (resumeEpisodeItem != null) {
334 logger.debug("Resuming series '{}' episode '{}'", seriesItem.getName(), resumeEpisodeItem.getName());
335 playItem(resumeEpisodeItem, playCommand,
336 Objects.requireNonNull(resumeEpisodeItem.getUserData()).getPlaybackPositionTicks());
337 } else if (nextUpEpisodeItem != null) {
338 logger.debug("Playing next series '{}' episode '{}'", seriesItem.getName(),
339 nextUpEpisodeItem.getName());
340 playItem(nextUpEpisodeItem, playCommand);
341 } else if (firstEpisodeItem != null) {
342 logger.debug("Playing series '{}' first episode '{}'", seriesItem.getName(),
343 firstEpisodeItem.getName());
344 playItem(firstEpisodeItem, playCommand);
346 logger.warn("Unable to found episode for series");
349 logger.debug("Browse series '{}'", seriesItem.getName());
350 browseItem(seriesItem);
354 private void runSeriesEpisode(String terms, int season, int episode, @Nullable PlayCommand playCommand)
355 throws SyncCallback.SyncCallbackError, ApiClientException {
356 logger.debug("{} series episode mode", playCommand != null ? "Play" : "Browse");
357 var seriesItem = getServerHandler().searchItem(terms, BaseItemKind.SERIES, null);
358 if (seriesItem != null) {
359 logger.debug("Searching series {} episode {}x{}", seriesItem.getName(), season, episode);
360 var episodeItem = getServerHandler().getSeriesEpisodeItem(seriesItem.getId(), season, episode);
361 if (episodeItem != null) {
362 runItem(episodeItem, playCommand);
364 logger.warn("Series {} episode {}x{} not found", seriesItem.getName(), season, episode);
367 logger.warn("Series not found");
371 private void runItem(BaseItemDto item, @Nullable PlayCommand playCommand)
372 throws SyncCallback.SyncCallbackError, ApiClientException {
373 var itemType = Objects.requireNonNull(item.getType());
374 logger.debug("{} {} '{}'", playCommand == null ? "Browsing" : "Playing", itemType.toString().toLowerCase(),
375 BaseItemKind.EPISODE.equals(itemType) ? item.getSeriesName() + ": " + item.getName() : item.getName());
376 if (playCommand == null) {
379 playItem(item, playCommand);
383 private void playItem(BaseItemDto item, PlayCommand playCommand)
384 throws SyncCallback.SyncCallbackError, ApiClientException {
385 playItem(item, playCommand, null);
388 private void playItem(BaseItemDto item, PlayCommand playCommand, @Nullable Long startPositionTicks)
389 throws SyncCallback.SyncCallbackError, ApiClientException {
390 if (playCommand.equals(PlayCommand.PLAY_NOW) && stopCurrentPlayback()) {
391 cancelDelayedCommand();
392 delayedCommand = scheduler.schedule(() -> {
394 playItemInternal(item, playCommand, startPositionTicks);
395 } catch (SyncCallback.SyncCallbackError | ApiClientException e) {
396 logger.warn("Unexpected error while running channel {}: {}", PLAY_BY_TERMS_CHANNEL, e.getMessage());
398 }, 3, TimeUnit.SECONDS);
400 playItemInternal(item, playCommand, startPositionTicks);
404 private void playItemInternal(BaseItemDto item, PlayCommand playCommand, @Nullable Long startPositionTicks)
405 throws SyncCallback.SyncCallbackError, ApiClientException {
406 getServerHandler().playItem(lastSessionId, playCommand, item.getId().toString(), startPositionTicks);
409 private void runItemById(UUID itemId, @Nullable PlayCommand playCommand)
410 throws SyncCallback.SyncCallbackError, ApiClientException {
411 var item = getServerHandler().getItem(itemId, null);
413 logger.warn("Unable to find item with id: {}", itemId);
416 if (BaseItemKind.SERIES.equals(item.getType())) {
417 runSeriesItem(item, playCommand);
419 runItem(item, playCommand);
423 private void browseItem(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
424 if (stopCurrentPlayback()) {
425 cancelDelayedCommand();
426 delayedCommand = scheduler.schedule(() -> {
428 browseItemInternal(item);
429 } catch (SyncCallback.SyncCallbackError | ApiClientException e) {
430 logger.warn("Unexpected error while running channel {}: {}", BROWSE_ITEM_BY_TERMS_CHANNEL,
433 }, 3, TimeUnit.SECONDS);
435 browseItemInternal(item);
439 private void browseItemInternal(BaseItemDto item) throws SyncCallback.SyncCallbackError, ApiClientException {
440 getServerHandler().browseToItem(lastSessionId, Objects.requireNonNull(item.getType()), item.getId().toString(),
441 Objects.requireNonNull(item.getName()));
444 private boolean stopCurrentPlayback() throws SyncCallback.SyncCallbackError, ApiClientException {
445 if (lastPlayingState) {
446 sendPlayStateCommand(PlaystateCommand.STOP);
452 private void sendPlayStateCommand(PlaystateCommand command)
453 throws SyncCallback.SyncCallbackError, ApiClientException {
454 sendPlayStateCommand(command, null);
457 private void sendPlayStateCommand(PlaystateCommand command, @Nullable Long seekPositionTick)
458 throws SyncCallback.SyncCallbackError, ApiClientException {
459 getServerHandler().sendPlayStateCommand(lastSessionId, command, seekPositionTick);
462 private void sendDeviceMessage(Command command) throws SyncCallback.SyncCallbackError, ApiClientException {
463 getServerHandler().sendDeviceMessage(lastSessionId, "Jellyfin OpenHAB", command.toFullString(), 15000);
466 private void handleMediaControlCommand(ChannelUID channelUID, Command command)
467 throws SyncCallback.SyncCallbackError, ApiClientException {
468 if (command instanceof RefreshType) {
470 } else if (command instanceof PlayPauseType) {
471 if (command == PlayPauseType.PLAY) {
472 sendPlayStateCommand(PlaystateCommand.UNPAUSE);
473 updateState(channelUID, PlayPauseType.PLAY);
474 } else if (command == PlayPauseType.PAUSE) {
475 sendPlayStateCommand(PlaystateCommand.PAUSE);
476 updateState(channelUID, PlayPauseType.PAUSE);
478 } else if (command instanceof NextPreviousType) {
479 if (command == NextPreviousType.NEXT) {
480 sendPlayStateCommand(PlaystateCommand.NEXT_TRACK);
481 } else if (command == NextPreviousType.PREVIOUS) {
482 sendPlayStateCommand(PlaystateCommand.PREVIOUS_TRACK);
484 } else if (command instanceof RewindFastforwardType) {
485 if (command == RewindFastforwardType.FASTFORWARD) {
486 sendPlayStateCommand(PlaystateCommand.FAST_FORWARD);
487 } else if (command == RewindFastforwardType.REWIND) {
488 sendPlayStateCommand(PlaystateCommand.REWIND);
491 logger.warn("Unknown media control command: {}", command);
495 private void seekToPercentage(Command command)
496 throws NumberFormatException, SyncCallback.SyncCallbackError, ApiClientException {
497 if (command.toFullString().equals(UnDefType.NULL.toFullString())) {
500 if (lastRunTimeTicks == 0L) {
501 logger.warn("Can't seek missing RunTimeTicks info");
504 int percentage = Integer.parseInt(command.toFullString());
505 var seekPositionTick = Math.round(((float) lastRunTimeTicks) * ((float) percentage / 100.0));
506 logger.debug("Seek to {}%: {} of {}", percentage, seekPositionTick, lastRunTimeTicks);
507 seekToTick(seekPositionTick);
510 private void seekToSecond(Command command)
511 throws NumberFormatException, SyncCallback.SyncCallbackError, ApiClientException {
512 if (command.toFullString().equals(UnDefType.NULL.toFullString())) {
515 long second = Long.parseLong(command.toFullString());
516 long seekPositionTick = second * 10000000L;
517 logger.debug("Seek to second {}: {} of {}", second, seekPositionTick, lastRunTimeTicks);
518 seekToTick(seekPositionTick);
521 private void seekToTick(long seekPositionTick) throws SyncCallback.SyncCallbackError, ApiClientException {
522 sendPlayStateCommand(PlaystateCommand.SEEK, seekPositionTick);
523 scheduler.schedule(this::refreshState, 3, TimeUnit.SECONDS);
526 private void cleanChannels() {
527 List.of(MEDIA_CONTROL_CHANNEL, PLAYING_ITEM_PERCENTAGE_CHANNEL, PLAYING_ITEM_ID_CHANNEL,
528 PLAYING_ITEM_NAME_CHANNEL, PLAYING_ITEM_SERIES_NAME_CHANNEL, PLAYING_ITEM_SEASON_NAME_CHANNEL,
529 PLAYING_ITEM_SEASON_CHANNEL, PLAYING_ITEM_EPISODE_CHANNEL, PLAYING_ITEM_GENRES_CHANNEL,
530 PLAYING_ITEM_TYPE_CHANNEL, PLAYING_ITEM_SECOND_CHANNEL, PLAYING_ITEM_TOTAL_SECOND_CHANNEL)
531 .forEach(this::cleanChannel);
534 private void cleanChannel(String channelId) {
535 updateState(new ChannelUID(this.thing.getUID(), channelId), UnDefType.NULL);
538 private JellyfinServerHandler getServerHandler() {
539 var bridge = Objects.requireNonNull(getBridge());
540 return (JellyfinServerHandler) Objects.requireNonNull(bridge.getHandler());