]> git.basschouten.com Git - openhab-addons.git/blob
188207203d8670a215122db6c5a113067123bc82
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.kodi.internal.protocol;
14
15 import java.io.IOException;
16 import java.math.BigDecimal;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.net.URLEncoder;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.concurrent.ScheduledExecutorService;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.websocket.client.WebSocketClient;
29 import org.openhab.binding.kodi.internal.KodiEventListener;
30 import org.openhab.binding.kodi.internal.KodiEventListener.KodiPlaylistState;
31 import org.openhab.binding.kodi.internal.KodiEventListener.KodiState;
32 import org.openhab.binding.kodi.internal.model.KodiAudioStream;
33 import org.openhab.binding.kodi.internal.model.KodiDuration;
34 import org.openhab.binding.kodi.internal.model.KodiFavorite;
35 import org.openhab.binding.kodi.internal.model.KodiPVRChannel;
36 import org.openhab.binding.kodi.internal.model.KodiPVRChannelGroup;
37 import org.openhab.binding.kodi.internal.model.KodiProfile;
38 import org.openhab.binding.kodi.internal.model.KodiSubtitle;
39 import org.openhab.binding.kodi.internal.model.KodiSystemProperties;
40 import org.openhab.binding.kodi.internal.model.KodiUniqueID;
41 import org.openhab.binding.kodi.internal.model.KodiVideoStream;
42 import org.openhab.core.cache.ByteArrayFileCache;
43 import org.openhab.core.cache.ExpiringCacheMap;
44 import org.openhab.core.io.net.http.HttpUtil;
45 import org.openhab.core.library.types.RawType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import com.google.gson.Gson;
50 import com.google.gson.JsonArray;
51 import com.google.gson.JsonElement;
52 import com.google.gson.JsonObject;
53 import com.google.gson.JsonPrimitive;
54 import com.google.gson.JsonSyntaxException;
55
56 /**
57  * KodiConnection provides an API for accessing a Kodi device.
58  *
59  * @author Paul Frank - Initial contribution
60  * @author Christoph Weitkamp - Added channels for opening PVR TV or Radio streams
61  * @author Andreas Reinhardt & Christoph Weitkamp - Added channels for thumbnail and fanart
62  * @author Christoph Weitkamp - Improvements for playing audio notifications
63  */
64 public class KodiConnection implements KodiClientSocketEventListener {
65
66     private static final String TIMESTAMP_FRAGMENT = "#timestamp=";
67     private static final String PROPERTY_FANART = "fanart";
68     private static final String PROPERTY_THUMBNAIL = "thumbnail";
69     private static final String PROPERTY_VERSION = "version";
70     private static final String PROPERTY_VOLUME = "volume";
71     private static final String PROPERTY_MUTED = "muted";
72     private static final String PROPERTY_TOTALTIME = "totaltime";
73     private static final String PROPERTY_TIME = "time";
74     private static final String PROPERTY_PERCENTAGE = "percentage";
75     private static final String PROPERTY_SUBTITLEENABLED = "subtitleenabled";
76     private static final String PROPERTY_CURRENTSUBTITLE = "currentsubtitle";
77     private static final String PROPERTY_CURRENTVIDEOSTREAM = "currentvideostream";
78     private static final String PROPERTY_CURRENTAUDIOSTREAM = "currentaudiostream";
79     private static final String PROPERTY_SUBTITLES = "subtitles";
80     private static final String PROPERTY_AUDIOSTREAMS = "audiostreams";
81     private static final String PROPERTY_CANHIBERNATE = "canhibernate";
82     private static final String PROPERTY_CANREBOOT = "canreboot";
83     private static final String PROPERTY_CANSHUTDOWN = "canshutdown";
84     private static final String PROPERTY_CANSUSPEND = "cansuspend";
85     private static final String PROPERTY_UNIQUEID = "uniqueid";
86
87     private final Logger logger = LoggerFactory.getLogger(KodiConnection.class);
88
89     private static final int VOLUMESTEP = 10;
90     // 0 = STOP or -1 = PLAY BACKWARDS are valid as well, but we don't want use them for FAST FORWARD or REWIND speeds
91     private static final List<Integer> SPEEDS = Arrays
92             .asList(new Integer[] { -32, -16, -8, -4, -2, 1, 2, 4, 8, 16, 32 });
93     private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.kodi");
94     private static final ExpiringCacheMap<String, JsonElement> REQUEST_CACHE = new ExpiringCacheMap<>(
95             TimeUnit.MINUTES.toMillis(5));
96
97     private final Gson gson = new Gson();
98
99     private String hostname;
100     private URI wsUri;
101     private URI imageUri;
102     private KodiClientSocket socket;
103
104     private int volume = 0;
105     private KodiState currentState = KodiState.STOP;
106     private KodiPlaylistState currentPlaylistState = KodiPlaylistState.CLEAR;
107
108     private final KodiEventListener listener;
109     private final WebSocketClient webSocketClient;
110     private final String callbackUrl;
111
112     public KodiConnection(KodiEventListener listener, WebSocketClient webSocketClient, String callbackUrl) {
113         this.listener = listener;
114         this.webSocketClient = webSocketClient;
115         this.callbackUrl = callbackUrl;
116     }
117
118     @Override
119     public synchronized void onConnectionClosed() {
120         listener.updateConnectionState(false);
121     }
122
123     @Override
124     public synchronized void onConnectionOpened() {
125         listener.updateConnectionState(true);
126     }
127
128     public synchronized void connect(String hostname, int port, ScheduledExecutorService scheduler, URI imageUri) {
129         this.hostname = hostname;
130         this.imageUri = imageUri;
131         try {
132             close();
133             wsUri = new URI("ws", null, hostname, port, "/jsonrpc", null, null);
134             socket = new KodiClientSocket(this, wsUri, scheduler, webSocketClient);
135             checkConnection();
136         } catch (URISyntaxException e) {
137             logger.warn("exception during constructing URI host={}, port={}", hostname, port, e);
138         }
139     }
140
141     private int getActivePlayer() {
142         JsonElement response = socket.callMethod("Player.GetActivePlayers");
143
144         if (response instanceof JsonArray) {
145             JsonArray result = response.getAsJsonArray();
146             if (result.size() > 0) {
147                 JsonObject player0 = result.get(0).getAsJsonObject();
148                 if (player0.has("playerid")) {
149                     return player0.get("playerid").getAsInt();
150                 }
151             }
152         }
153         return -1;
154     }
155
156     public int getActivePlaylist() {
157         for (JsonElement element : getPlaylistsInternal()) {
158             JsonObject playlist = (JsonObject) element;
159             if (playlist.has("playlistid")) {
160                 int playlistID = playlist.get("playlistid").getAsInt();
161                 JsonObject playlistItems = getPlaylistItemsInternal(playlistID);
162                 if (playlistItems.has("limits") && playlistItems.get("limits") instanceof JsonObject) {
163                     JsonObject limits = playlistItems.get("limits").getAsJsonObject();
164                     if (limits.has("total") && limits.get("total").getAsInt() > 0) {
165                         return playlistID;
166                     }
167                 }
168             }
169         }
170         return -1;
171     }
172
173     public int getPlaylistID(String type) {
174         for (JsonElement element : getPlaylistsInternal()) {
175             JsonObject playlist = (JsonObject) element;
176             if (playlist.has("playlistid") && playlist.has("type") && type.equals(playlist.get("type").getAsString())) {
177                 return playlist.get("playlistid").getAsInt();
178             }
179         }
180         return -1;
181     }
182
183     private synchronized JsonArray getPlaylistsInternal() {
184         String method = "Playlist.GetPlaylists";
185         String hash = hostname + '#' + method;
186         JsonElement response = REQUEST_CACHE.putIfAbsentAndGet(hash, () -> {
187             return socket.callMethod(method);
188         });
189
190         if (response instanceof JsonArray) {
191             return response.getAsJsonArray();
192         } else {
193             return null;
194         }
195     }
196
197     private synchronized JsonObject getPlaylistItemsInternal(int playlistID) {
198         JsonObject params = new JsonObject();
199         params.addProperty("playlistid", playlistID);
200         JsonElement response = socket.callMethod("Playlist.GetItems", params);
201
202         if (response instanceof JsonObject) {
203             return response.getAsJsonObject();
204         } else {
205             return null;
206         }
207     }
208
209     public synchronized void playerPlayPause() {
210         int activePlayer = getActivePlayer();
211
212         JsonObject params = new JsonObject();
213         params.addProperty("playerid", activePlayer);
214         socket.callMethod("Player.PlayPause", params);
215     }
216
217     public synchronized void playerStop() {
218         int activePlayer = getActivePlayer();
219
220         JsonObject params = new JsonObject();
221         params.addProperty("playerid", activePlayer);
222         socket.callMethod("Player.Stop", params);
223     }
224
225     public synchronized void playerNext() {
226         goToInternal("next");
227
228         updatePlayerStatus();
229     }
230
231     public synchronized void playerPrevious() {
232         goToInternal("previous");
233
234         updatePlayerStatus();
235     }
236
237     private void goToInternal(String to) {
238         int activePlayer = getActivePlayer();
239
240         JsonObject params = new JsonObject();
241         params.addProperty("playerid", activePlayer);
242         params.addProperty("to", to);
243         socket.callMethod("Player.GoTo", params);
244     }
245
246     public synchronized void playerRewind() {
247         setSpeedInternal(calcNextSpeed(-1));
248
249         updatePlayerStatus();
250     }
251
252     public synchronized void playerFastForward() {
253         setSpeedInternal(calcNextSpeed(1));
254
255         updatePlayerStatus();
256     }
257
258     private int calcNextSpeed(int modifier) {
259         int activePlayer = getActivePlayer();
260         if (activePlayer >= 0) {
261             int position = SPEEDS.indexOf(getSpeed(activePlayer));
262             if (position == -1) {
263                 return 0;
264             } else if (position == 0 || position == (SPEEDS.size() - 1)) {
265                 return SPEEDS.get(position);
266             } else {
267                 return SPEEDS.get(position + modifier);
268             }
269         } else {
270             return 0;
271         }
272     }
273
274     private void setSpeedInternal(int speed) {
275         int activePlayer = getActivePlayer();
276
277         JsonObject params = new JsonObject();
278         params.addProperty("playerid", activePlayer);
279         params.addProperty("speed", speed);
280         socket.callMethod("Player.SetSpeed", params);
281     }
282
283     public synchronized void playlistAdd(int playlistID, String uri) {
284         currentPlaylistState = KodiPlaylistState.ADD;
285
286         JsonObject item = new JsonObject();
287         item.addProperty("file", uri);
288
289         JsonObject params = new JsonObject();
290         params.addProperty("playlistid", playlistID);
291         params.add("item", item);
292         socket.callMethod("Playlist.Add", params);
293     }
294
295     public synchronized void playlistClear(int playlistID) {
296         currentPlaylistState = KodiPlaylistState.CLEAR;
297
298         JsonObject params = new JsonObject();
299         params.addProperty("playlistid", playlistID);
300         socket.callMethod("Playlist.Clear", params);
301     }
302
303     public synchronized void playlistInsert(int playlistID, String uri, int position) {
304         currentPlaylistState = KodiPlaylistState.INSERT;
305
306         JsonObject item = new JsonObject();
307         item.addProperty("file", uri);
308
309         JsonObject params = new JsonObject();
310         params.addProperty("playlistid", playlistID);
311         params.addProperty("position", position);
312         params.add("item", item);
313         socket.callMethod("Playlist.Insert", params);
314     }
315
316     public synchronized void playlistPlay(int playlistID, int position) {
317         JsonObject item = new JsonObject();
318         item.addProperty("playlistid", playlistID);
319         item.addProperty("position", position);
320
321         playInternal(item, null);
322     }
323
324     public synchronized void playlistRemove(int playlistID, int position) {
325         currentPlaylistState = KodiPlaylistState.REMOVE;
326
327         JsonObject params = new JsonObject();
328         params.addProperty("playlistid", playlistID);
329         params.addProperty("position", position);
330         socket.callMethod("Playlist.Remove", params);
331     }
332
333     /**
334      * Retrieves a list of favorites from the Kodi instance. The result is cached.
335      *
336      * @return a list of {@link KodiFavorite}
337      */
338     public synchronized List<KodiFavorite> getFavorites() {
339         String method = "Favourites.GetFavourites";
340         String hash = hostname + '#' + method;
341         JsonElement response = REQUEST_CACHE.putIfAbsentAndGet(hash, () -> {
342             final String[] properties = { "path", "window", "windowparameter" };
343
344             JsonObject params = new JsonObject();
345             params.add("properties", getJsonArray(properties));
346             return socket.callMethod(method, params);
347         });
348
349         List<KodiFavorite> favorites = new ArrayList<>();
350         if (response instanceof JsonObject) {
351             JsonObject result = response.getAsJsonObject();
352             if (result.has("favourites")) {
353                 JsonElement favourites = result.get("favourites");
354                 if (favourites instanceof JsonArray) {
355                     for (JsonElement element : favourites.getAsJsonArray()) {
356                         JsonObject object = (JsonObject) element;
357                         KodiFavorite favorite = new KodiFavorite(object.get("title").getAsString());
358                         favorite.setFavoriteType(object.get("type").getAsString());
359                         if (object.has("path")) {
360                             favorite.setPath(object.get("path").getAsString());
361                         }
362                         if (object.has("window")) {
363                             favorite.setWindow(object.get("window").getAsString());
364                             favorite.setWindowParameter(object.get("windowparameter").getAsString());
365                         }
366                         favorites.add(favorite);
367                     }
368                 }
369             }
370         }
371         return favorites;
372     }
373
374     /**
375      * Returns the favorite with the given title or null.
376      *
377      * @param favoriteTitle the title of the favorite
378      * @return the ({@link KodiFavorite}) with the given title
379      */
380     public @Nullable KodiFavorite getFavorite(final String favoriteTitle) {
381         for (KodiFavorite favorite : getFavorites()) {
382             String title = favorite.getTitle();
383             if (favoriteTitle.equalsIgnoreCase(title)) {
384                 return favorite;
385             }
386         }
387         return null;
388     }
389
390     /**
391      * Activates the given window.
392      *
393      * @param window the window
394      */
395     public synchronized void activateWindow(final String window) {
396         activateWindow(window, null);
397     }
398
399     /**
400      * Activates the given window.
401      *
402      * @param window the window
403      * @param windowParameter list of parameters of the window
404      */
405     public synchronized void activateWindow(final String window, @Nullable final String[] windowParameter) {
406         JsonObject params = new JsonObject();
407         params.addProperty("window", window);
408         if (windowParameter != null) {
409             params.add("parameters", getJsonArray(windowParameter));
410         }
411         socket.callMethod("GUI.ActivateWindow", params);
412     }
413
414     public synchronized void increaseVolume() {
415         setVolumeInternal(this.volume + VOLUMESTEP);
416     }
417
418     public synchronized void decreaseVolume() {
419         setVolumeInternal(this.volume - VOLUMESTEP);
420     }
421
422     public synchronized void setVolume(int volume) {
423         setVolumeInternal(volume);
424     }
425
426     private void setVolumeInternal(int volume) {
427         JsonObject params = new JsonObject();
428         params.addProperty(PROPERTY_VOLUME, volume);
429         socket.callMethod("Application.SetVolume", params);
430     }
431
432     public int getVolume() {
433         return volume;
434     }
435
436     public synchronized void setMute(boolean mute) {
437         JsonObject params = new JsonObject();
438         params.addProperty("mute", mute);
439         socket.callMethod("Application.SetMute", params);
440     }
441
442     public synchronized void setAudioStream(int stream) {
443         JsonObject params = new JsonObject();
444         params.addProperty("stream", stream);
445         int activePlayer = getActivePlayer();
446         params.addProperty("playerid", activePlayer);
447         socket.callMethod("Player.SetAudioStream", params);
448     }
449
450     public synchronized void setVideoStream(int stream) {
451         JsonObject params = new JsonObject();
452         params.addProperty("stream", stream);
453         int activePlayer = getActivePlayer();
454         params.addProperty("playerid", activePlayer);
455         socket.callMethod("Player.SetVideoStream", params);
456     }
457
458     public synchronized void setSubtitle(int subtitle) {
459         JsonObject params = new JsonObject();
460         params.addProperty("subtitle", subtitle);
461         int activePlayer = getActivePlayer();
462         params.addProperty("playerid", activePlayer);
463         socket.callMethod("Player.SetSubtitle", params);
464     }
465
466     public synchronized void setSubtitleEnabled(boolean subtitleenabled) {
467         JsonObject params = new JsonObject();
468         params.addProperty("subtitle", subtitleenabled ? "on" : "off");
469         int activePlayer = getActivePlayer();
470         params.addProperty("playerid", activePlayer);
471         socket.callMethod("Player.SetSubtitle", params);
472     }
473
474     private int getSpeed(int activePlayer) {
475         final String[] properties = { "speed" };
476
477         JsonObject params = new JsonObject();
478         params.addProperty("playerid", activePlayer);
479         params.add("properties", getJsonArray(properties));
480         JsonElement response = socket.callMethod("Player.GetProperties", params);
481
482         if (response instanceof JsonObject) {
483             JsonObject result = response.getAsJsonObject();
484             if (result.has("speed")) {
485                 return result.get("speed").getAsInt();
486             }
487         }
488         return 0;
489     }
490
491     public synchronized void updatePlayerStatus() {
492         if (socket.isConnected()) {
493             int activePlayer = getActivePlayer();
494             if (activePlayer >= 0) {
495                 int speed = getSpeed(activePlayer);
496                 if (speed == 0) {
497                     updateState(KodiState.STOP);
498                 } else if (speed == 1) {
499                     updateState(KodiState.PLAY);
500                 } else if (speed < 0) {
501                     updateState(KodiState.REWIND);
502                 } else {
503                     updateState(KodiState.FASTFORWARD);
504                 }
505                 requestPlayerUpdate(activePlayer);
506             } else {
507                 updateState(KodiState.STOP);
508             }
509         }
510     }
511
512     private void requestPlayerUpdate(int activePlayer) {
513         requestPlayerPropertiesUpdate(activePlayer);
514         requestPlayerItemUpdate(activePlayer);
515     }
516
517     private void requestPlayerItemUpdate(int activePlayer) {
518         final String[] properties = { PROPERTY_UNIQUEID, "title", "originaltitle", "album", "artist", "track",
519                 "director", PROPERTY_THUMBNAIL, PROPERTY_FANART, "file", "showtitle", "season", "episode", "channel",
520                 "channeltype", "genre", "mpaa", "rating", "votes", "userrating" };
521
522         JsonObject params = new JsonObject();
523         params.addProperty("playerid", activePlayer);
524         params.add("properties", getJsonArray(properties));
525         JsonElement response = socket.callMethod("Player.GetItem", params);
526
527         if (response instanceof JsonObject) {
528             JsonObject result = response.getAsJsonObject();
529             if (result.has("item")) {
530                 JsonObject item = result.get("item").getAsJsonObject();
531
532                 int mediaid = -1;
533                 if (item.has("id")) {
534                     mediaid = item.get("id").getAsInt();
535                 }
536
537                 double rating = -1;
538                 if (item.has("rating")) {
539                     rating = item.get("rating").getAsDouble();
540                 }
541
542                 double userrating = -1;
543                 if (item.has("userrating")) {
544                     userrating = item.get("userrating").getAsDouble();
545                 }
546
547                 String mpaa = "";
548                 if (item.has("mpaa")) {
549                     mpaa = item.get("mpaa").getAsString();
550                 }
551
552                 String mediafile = "";
553                 if (item.has("file")) {
554                     mediafile = item.get("file").getAsString();
555                 }
556
557                 String uniqueIDDouban = "";
558                 String uniqueIDImdb = "";
559                 String uniqueIDTmdb = "";
560                 String uniqueIDImdbtvshow = "";
561                 String uniqueIDTmdbtvshow = "";
562                 String uniqueIDTmdbepisode = "";
563
564                 if (item.has(PROPERTY_UNIQUEID)) {
565                     try {
566                         KodiUniqueID uniqueID = gson.fromJson(item.get(PROPERTY_UNIQUEID), KodiUniqueID.class);
567                         if (uniqueID != null) {
568                             uniqueIDImdb = uniqueID.getImdb();
569                             uniqueIDDouban = uniqueID.getDouban();
570                             uniqueIDTmdb = uniqueID.getTmdb();
571                             uniqueIDImdbtvshow = uniqueID.getImdbtvshow();
572                             uniqueIDTmdbtvshow = uniqueID.getTmdbtvshow();
573                             uniqueIDTmdbepisode = uniqueID.getTmdbepisode();
574                         }
575                     } catch (JsonSyntaxException e) {
576                         // do nothing
577                     }
578                 }
579
580                 String originaltitle = "";
581                 if (item.has("originaltitle")) {
582                     originaltitle = item.get("originaltitle").getAsString();
583                 }
584
585                 String title = "";
586                 if (item.has("title")) {
587                     title = item.get("title").getAsString();
588                 }
589                 if (title.isEmpty()) {
590                     title = item.get("label").getAsString();
591                 }
592
593                 String showTitle = "";
594                 if (item.has("showtitle")) {
595                     showTitle = item.get("showtitle").getAsString();
596                 }
597
598                 int season = -1;
599                 if (item.has("season")) {
600                     season = item.get("season").getAsInt();
601                 }
602
603                 int episode = -1;
604                 if (item.has("episode")) {
605                     episode = item.get("episode").getAsInt();
606                 }
607
608                 String album = "";
609                 if (item.has("album")) {
610                     album = item.get("album").getAsString();
611                 }
612
613                 String mediaType = item.get("type").getAsString();
614                 if ("channel".equals(mediaType) && item.has("channeltype")) {
615                     String channelType = item.get("channeltype").getAsString();
616                     if ("radio".equals(channelType)) {
617                         mediaType = "radio";
618                     }
619                 }
620
621                 List<String> artistList = null;
622                 if ("movie".equals(mediaType) && item.has("director")) {
623                     artistList = convertFromArrayToList(item.get("director").getAsJsonArray());
624                 } else {
625                     if (item.has("artist")) {
626                         artistList = convertFromArrayToList(item.get("artist").getAsJsonArray());
627                     }
628                 }
629
630                 List<String> genreList = null;
631                 if (item.has("genre")) {
632                     JsonElement genre = item.get("genre");
633                     if (genre instanceof JsonArray) {
634                         genreList = convertFromArrayToList(genre.getAsJsonArray());
635                     }
636                 }
637
638                 String channel = "";
639                 if (item.has("channel")) {
640                     channel = item.get("channel").getAsString();
641                 }
642
643                 RawType thumbnail = null;
644                 if (item.has(PROPERTY_THUMBNAIL)) {
645                     thumbnail = getImageForElement(item.get(PROPERTY_THUMBNAIL));
646                 }
647
648                 RawType fanart = null;
649                 if (item.has(PROPERTY_FANART)) {
650                     fanart = getImageForElement(item.get(PROPERTY_FANART));
651                 }
652
653                 listener.updateMediaID(mediaid);
654                 listener.updateAlbum(album);
655                 listener.updateTitle(title);
656                 listener.updateOriginalTitle(originaltitle);
657                 listener.updateShowTitle(showTitle);
658                 listener.updateArtistList(artistList);
659                 listener.updateMediaType(mediaType);
660                 listener.updateGenreList(genreList);
661                 listener.updatePVRChannel(channel);
662                 listener.updateThumbnail(thumbnail);
663                 listener.updateFanart(fanart);
664                 listener.updateSeason(season);
665                 listener.updateEpisode(episode);
666                 listener.updateMediaFile(mediafile);
667                 listener.updateMpaa(mpaa);
668                 listener.updateRating(rating);
669                 listener.updateUserRating(userrating);
670                 listener.updateUniqueIDDouban(uniqueIDDouban);
671                 listener.updateUniqueIDImdb(uniqueIDImdb);
672                 listener.updateUniqueIDTmdb(uniqueIDTmdb);
673                 listener.updateUniqueIDImdbtvshow(uniqueIDImdbtvshow);
674                 listener.updateUniqueIDTmdbtvshow(uniqueIDTmdbtvshow);
675                 listener.updateUniqueIDTmdbepisode(uniqueIDTmdbepisode);
676             }
677         }
678     }
679
680     private void requestPlayerPropertiesUpdate(int activePlayer) {
681         final String[] properties = { PROPERTY_SUBTITLEENABLED, PROPERTY_CURRENTSUBTITLE, PROPERTY_CURRENTAUDIOSTREAM,
682                 PROPERTY_CURRENTVIDEOSTREAM, PROPERTY_PERCENTAGE, PROPERTY_TIME, PROPERTY_TOTALTIME,
683                 PROPERTY_AUDIOSTREAMS, PROPERTY_SUBTITLES };
684
685         JsonObject params = new JsonObject();
686         params.addProperty("playerid", activePlayer);
687         params.add("properties", getJsonArray(properties));
688         JsonElement response = socket.callMethod("Player.GetProperties", params);
689
690         if (response instanceof JsonObject) {
691             JsonObject result = response.getAsJsonObject();
692
693             if (result.has(PROPERTY_AUDIOSTREAMS)) {
694                 try {
695                     JsonElement audioGroup = result.get(PROPERTY_AUDIOSTREAMS);
696                     if (audioGroup instanceof JsonArray) {
697                         List<KodiAudioStream> audioStreamList = new ArrayList<>();
698                         for (JsonElement element : audioGroup.getAsJsonArray()) {
699                             KodiAudioStream audioStream = gson.fromJson(element, KodiAudioStream.class);
700                             audioStreamList.add(audioStream);
701                         }
702                         listener.updateAudioStreamOptions(audioStreamList);
703                     }
704                 } catch (JsonSyntaxException e) {
705                     // do nothing
706                 }
707             }
708
709             if (result.has(PROPERTY_SUBTITLES)) {
710                 try {
711                     JsonElement subtitleGroup = result.get(PROPERTY_SUBTITLES);
712                     if (subtitleGroup instanceof JsonArray) {
713                         List<KodiSubtitle> subtitleList = new ArrayList<>();
714                         for (JsonElement element : subtitleGroup.getAsJsonArray()) {
715                             KodiSubtitle subtitle = gson.fromJson(element, KodiSubtitle.class);
716                             subtitleList.add(subtitle);
717                         }
718                         listener.updateSubtitleOptions(subtitleList);
719                     }
720                 } catch (JsonSyntaxException e) {
721                     // do nothing
722                 }
723             }
724
725             boolean subtitleEnabled = false;
726             if (result.has(PROPERTY_SUBTITLEENABLED)) {
727                 subtitleEnabled = result.get(PROPERTY_SUBTITLEENABLED).getAsBoolean();
728             }
729
730             int subtitleIndex = -1;
731             String subtitleLanguage = null;
732             String subtitleName = null;
733             if (result.has(PROPERTY_CURRENTSUBTITLE)) {
734                 try {
735                     KodiSubtitle subtitleStream = gson.fromJson(result.get(PROPERTY_CURRENTSUBTITLE),
736                             KodiSubtitle.class);
737                     if (subtitleStream != null) {
738                         subtitleIndex = subtitleStream.getIndex();
739                         subtitleLanguage = subtitleStream.getLanguage();
740                         subtitleName = subtitleStream.getName();
741                     }
742                 } catch (JsonSyntaxException e) {
743                     // do nothing
744                 }
745             }
746
747             String audioCodec = null;
748             int audioIndex = -1;
749             int audioChannels = 0;
750             String audioLanguage = null;
751             String audioName = null;
752             if (result.has(PROPERTY_CURRENTAUDIOSTREAM)) {
753                 try {
754                     KodiAudioStream audioStream = gson.fromJson(result.get(PROPERTY_CURRENTAUDIOSTREAM),
755                             KodiAudioStream.class);
756                     if (audioStream != null) {
757                         audioCodec = audioStream.getCodec();
758                         audioIndex = audioStream.getIndex();
759                         audioChannels = audioStream.getChannels();
760                         audioLanguage = audioStream.getLanguage();
761                         audioName = audioStream.getName();
762                     }
763                 } catch (JsonSyntaxException e) {
764                     // do nothing
765                 }
766             }
767
768             String videoCodec = null;
769             int videoWidth = 0;
770             int videoHeight = 0;
771             int videoIndex = -1;
772             if (result.has(PROPERTY_CURRENTVIDEOSTREAM)) {
773                 try {
774                     KodiVideoStream videoStream = gson.fromJson(result.get(PROPERTY_CURRENTVIDEOSTREAM),
775                             KodiVideoStream.class);
776                     if (videoStream != null) {
777                         videoCodec = videoStream.getCodec();
778                         videoWidth = videoStream.getWidth();
779                         videoHeight = videoStream.getHeight();
780                         videoIndex = videoStream.getIndex();
781                     }
782                 } catch (JsonSyntaxException e) {
783                     // do nothing
784                 }
785             }
786
787             double percentage = -1;
788             if (result.has(PROPERTY_PERCENTAGE)) {
789                 percentage = result.get(PROPERTY_PERCENTAGE).getAsDouble();
790             }
791
792             long currentTime = -1;
793             if (result.has(PROPERTY_TIME)) {
794                 try {
795                     KodiDuration time = gson.fromJson(result.get(PROPERTY_TIME), KodiDuration.class);
796                     currentTime = time.toSeconds();
797                 } catch (JsonSyntaxException e) {
798                     // do nothing
799                 }
800             }
801
802             long duration = -1;
803             if (result.has(PROPERTY_TOTALTIME)) {
804                 try {
805                     KodiDuration totalTime = gson.fromJson(result.get(PROPERTY_TOTALTIME), KodiDuration.class);
806                     duration = totalTime.toSeconds();
807                 } catch (JsonSyntaxException e) {
808                     // do nothing
809                 }
810             }
811
812             listener.updateAudioCodec(audioCodec);
813             listener.updateAudioIndex(audioIndex);
814             listener.updateAudioName(audioName);
815             listener.updateAudioLanguage(audioLanguage);
816             listener.updateAudioChannels(audioChannels);
817             listener.updateVideoCodec(videoCodec);
818             listener.updateVideoIndex(videoIndex);
819             listener.updateVideoHeight(videoHeight);
820             listener.updateVideoWidth(videoWidth);
821             listener.updateSubtitleEnabled(subtitleEnabled);
822             listener.updateSubtitleIndex(subtitleIndex);
823             listener.updateSubtitleName(subtitleName);
824             listener.updateSubtitleLanguage(subtitleLanguage);
825             listener.updateCurrentTimePercentage(percentage);
826             listener.updateCurrentTime(currentTime);
827             listener.updateDuration(duration);
828         }
829     }
830
831     private JsonArray getJsonArray(String[] values) {
832         JsonArray result = new JsonArray();
833         for (String param : values) {
834             result.add(new JsonPrimitive(param));
835         }
836         return result;
837     }
838
839     private List<String> convertFromArrayToList(JsonArray data) {
840         List<String> list = new ArrayList<>();
841         for (JsonElement element : data) {
842             list.add(element.getAsString());
843         }
844         return list;
845     }
846
847     private @Nullable RawType getImageForElement(JsonElement element) {
848         String text = element.getAsString();
849         if (!text.isEmpty()) {
850             String url = stripImageUrl(text);
851             if (url != null) {
852                 return downloadImageFromCache(url);
853             }
854         }
855         return null;
856     }
857
858     private @Nullable String stripImageUrl(String url) {
859         // we have to strip ending "/" here because Kodi returns a not valid path and filename
860         // "fanart":"image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f263365-31.jpg/"
861         // "thumbnail":"image://http%3a%2f%2fthetvdb.com%2fbanners%2fepisodes%2f263365%2f5640869.jpg/"
862         String encodedURL = URLEncoder.encode(stripEnd(url, '/'), StandardCharsets.UTF_8);
863         return imageUri.resolve(encodedURL).toString();
864     }
865
866     private String stripEnd(final String str, final char suffix) {
867         int end = str.length();
868         if (end == 0) {
869             return str;
870         }
871         while (end > 0 && str.charAt(end - 1) == suffix) {
872             end--;
873         }
874         return str.substring(0, end);
875     }
876
877     private @Nullable RawType downloadImage(String url) {
878         logger.debug("Trying to download the content of URL '{}'", url);
879         RawType downloadedImage = HttpUtil.downloadImage(url);
880         if (downloadedImage == null) {
881             logger.debug("Failed to download the content of URL '{}'", url);
882         }
883         return downloadedImage;
884     }
885
886     private @Nullable RawType downloadImageFromCache(String url) {
887         if (IMAGE_CACHE.containsKey(url)) {
888             try {
889                 byte[] bytes = IMAGE_CACHE.get(url);
890                 String contentType = HttpUtil.guessContentTypeFromData(bytes);
891                 return new RawType(bytes,
892                         contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType);
893             } catch (IOException e) {
894                 logger.trace("Failed to download the content of URL '{}'", url, e);
895             }
896         } else {
897             RawType image = downloadImage(url);
898             if (image != null) {
899                 IMAGE_CACHE.put(url, image.getBytes());
900                 return image;
901             }
902         }
903         return null;
904     }
905
906     public KodiState getState() {
907         return currentState;
908     }
909
910     public KodiPlaylistState getPlaylistState() {
911         return currentPlaylistState;
912     }
913
914     private void updateState(KodiState state) {
915         // sometimes get a Pause immediately after a Stop - so just ignore
916         if (currentState.equals(KodiState.STOP) && state.equals(KodiState.PAUSE)) {
917             return;
918         }
919         listener.updatePlayerState(state);
920         // if this is a Stop then clear everything else
921         if (state == KodiState.STOP) {
922             listener.updateAlbum("");
923             listener.updateTitle("");
924             listener.updateShowTitle("");
925             listener.updateArtistList(null);
926             listener.updateMediaType("");
927             listener.updateGenreList(null);
928             listener.updatePVRChannel("");
929             listener.updateThumbnail(null);
930             listener.updateFanart(null);
931             listener.updateCurrentTimePercentage(-1);
932             listener.updateCurrentTime(-1);
933             listener.updateDuration(-1);
934             listener.updateMediaID(-1);
935             listener.updateOriginalTitle("");
936             listener.updateSeason(-1);
937             listener.updateEpisode(-1);
938             listener.updateMediaFile("");
939             listener.updateMpaa("");
940             listener.updateRating(-1);
941             listener.updateUserRating(-1);
942             listener.updateUniqueIDDouban("");
943             listener.updateUniqueIDImdb("");
944             listener.updateUniqueIDTmdb("");
945             listener.updateUniqueIDImdbtvshow("");
946             listener.updateUniqueIDTmdbtvshow("");
947             listener.updateUniqueIDTmdbepisode("");
948             listener.updateAudioStreamOptions(new ArrayList<>());
949             listener.updateSubtitleOptions(new ArrayList<>());
950             listener.updateAudioCodec(null);
951             listener.updateVideoCodec(null);
952             listener.updateAudioIndex(-1);
953             listener.updateAudioName(null);
954             listener.updateAudioLanguage(null);
955             listener.updateAudioChannels(-1);
956             listener.updateVideoIndex(-1);
957             listener.updateVideoHeight(-1);
958             listener.updateVideoWidth(-1);
959             listener.updateSubtitleIndex(-1);
960             listener.updateSubtitleName(null);
961             listener.updateSubtitleLanguage(null);
962         }
963         // keep track of our current state
964         currentState = state;
965     }
966
967     @Override
968     public void handleEvent(JsonObject json) {
969         JsonElement methodElement = json.get("method");
970
971         if (methodElement != null) {
972             String method = methodElement.getAsString();
973             JsonObject params = json.get("params").getAsJsonObject();
974             if (method.startsWith("Player.On")) {
975                 processPlayerStateChanged(method, params);
976             } else if (method.startsWith("Application.On")) {
977                 processApplicationStateChanged(method, params);
978             } else if (method.startsWith("System.On")) {
979                 processSystemStateChanged(method, params);
980             } else if (method.startsWith("GUI.OnScreensaver")) {
981                 processScreensaverStateChanged(method, params);
982             } else if (method.startsWith("Playlist.On")) {
983                 processPlaylistStateChanged(method, params);
984             } else {
985                 logger.debug("Received unknown method: {}", method);
986             }
987         }
988     }
989
990     private void processPlayerStateChanged(String method, JsonObject json) {
991         if ("Player.OnPlay".equals(method)) {
992             // get the player id and make a new request for the media details
993
994             JsonObject data = json.get("data").getAsJsonObject();
995             JsonObject player = data.get("player").getAsJsonObject();
996             Integer playerId = player.get("playerid").getAsInt();
997
998             updateState(KodiState.PLAY);
999
1000             requestPlayerUpdate(playerId);
1001         } else if ("Player.OnPause".equals(method)) {
1002             updateState(KodiState.PAUSE);
1003         } else if ("Player.OnResume".equals(method)) {
1004             updateState(KodiState.PLAY);
1005         } else if ("Player.OnStop".equals(method)) {
1006             // get the end parameter and send an End state if true
1007             JsonObject data = json.get("data").getAsJsonObject();
1008             Boolean end = data.get("end").getAsBoolean();
1009             if (end) {
1010                 updateState(KodiState.END);
1011             }
1012             updateState(KodiState.STOP);
1013         } else if ("Player.OnPropertyChanged".equals(method)) {
1014             logger.debug("Player.OnPropertyChanged");
1015         } else if ("Player.OnSpeedChanged".equals(method)) {
1016             JsonObject data = json.get("data").getAsJsonObject();
1017             JsonObject player = data.get("player").getAsJsonObject();
1018             int speed = player.get("speed").getAsInt();
1019             if (speed == 0) {
1020                 updateState(KodiState.PAUSE);
1021             } else if (speed == 1) {
1022                 updateState(KodiState.PLAY);
1023             } else if (speed < 0) {
1024                 updateState(KodiState.REWIND);
1025             } else if (speed > 1) {
1026                 updateState(KodiState.FASTFORWARD);
1027             }
1028         } else {
1029             logger.debug("Unknown event from Kodi {}: {}", method, json);
1030         }
1031         listener.updateConnectionState(true);
1032     }
1033
1034     private void processApplicationStateChanged(String method, JsonObject json) {
1035         if ("Application.OnVolumeChanged".equals(method)) {
1036             // get the player id and make a new request for the media details
1037             JsonObject data = json.get("data").getAsJsonObject();
1038             if (data.has(PROPERTY_VOLUME)) {
1039                 volume = data.get(PROPERTY_VOLUME).getAsInt();
1040                 listener.updateVolume(volume);
1041             }
1042             if (data.has(PROPERTY_MUTED)) {
1043                 boolean muted = data.get(PROPERTY_MUTED).getAsBoolean();
1044                 listener.updateMuted(muted);
1045             }
1046         } else {
1047             logger.debug("Unknown event from Kodi {}: {}", method, json);
1048         }
1049         listener.updateConnectionState(true);
1050     }
1051
1052     private void processSystemStateChanged(String method, JsonObject json) {
1053         if ("System.OnQuit".equals(method) || "System.OnRestart".equals(method) || "System.OnSleep".equals(method)) {
1054             listener.updateConnectionState(false);
1055         } else if ("System.OnWake".equals(method)) {
1056             listener.updateConnectionState(true);
1057         } else {
1058             logger.debug("Unknown event from Kodi {}: {}", method, json);
1059         }
1060     }
1061
1062     private void processScreensaverStateChanged(String method, JsonObject json) {
1063         if ("GUI.OnScreensaverDeactivated".equals(method)) {
1064             listener.updateScreenSaverState(false);
1065         } else if ("GUI.OnScreensaverActivated".equals(method)) {
1066             listener.updateScreenSaverState(true);
1067         } else {
1068             logger.debug("Unknown event from Kodi {}: {}", method, json);
1069         }
1070         listener.updateConnectionState(true);
1071     }
1072
1073     private void processPlaylistStateChanged(String method, JsonObject json) {
1074         if ("Playlist.OnAdd".equals(method)) {
1075             currentPlaylistState = KodiPlaylistState.ADDED;
1076
1077             listener.updatePlaylistState(KodiPlaylistState.ADDED);
1078         } else if ("Playlist.OnRemove".equals(method)) {
1079             currentPlaylistState = KodiPlaylistState.REMOVED;
1080
1081             listener.updatePlaylistState(KodiPlaylistState.REMOVED);
1082         } else {
1083             logger.debug("Unknown event from Kodi {}: {}", method, json);
1084         }
1085         listener.updateConnectionState(true);
1086     }
1087
1088     public synchronized void close() {
1089         if (socket != null && socket.isConnected()) {
1090             socket.close();
1091         }
1092     }
1093
1094     public void updateVolume() {
1095         if (socket.isConnected()) {
1096             String[] props = { PROPERTY_VOLUME, PROPERTY_MUTED };
1097
1098             JsonObject params = new JsonObject();
1099             params.add("properties", getJsonArray(props));
1100             JsonElement response = socket.callMethod("Application.GetProperties", params);
1101
1102             if (response instanceof JsonObject) {
1103                 JsonObject data = response.getAsJsonObject();
1104                 if (data.has(PROPERTY_VOLUME)) {
1105                     volume = data.get(PROPERTY_VOLUME).getAsInt();
1106                     listener.updateVolume(volume);
1107                 }
1108                 if (data.has(PROPERTY_MUTED)) {
1109                     boolean muted = data.get(PROPERTY_MUTED).getAsBoolean();
1110                     listener.updateMuted(muted);
1111                 }
1112             }
1113         } else {
1114             listener.updateVolume(100);
1115             listener.updateMuted(false);
1116         }
1117     }
1118
1119     public void updateCurrentProfile() {
1120         if (socket.isConnected()) {
1121             JsonElement response = socket.callMethod("Profiles.GetCurrentProfile");
1122
1123             try {
1124                 final KodiProfile profile = gson.fromJson(response, KodiProfile.class);
1125                 if (profile != null) {
1126                     listener.updateCurrentProfile(profile.getLabel());
1127                 }
1128             } catch (JsonSyntaxException e) {
1129                 logger.debug("Json syntax exception occurred: {}", e.getMessage(), e);
1130             }
1131         }
1132     }
1133
1134     public synchronized void playURI(String uri) {
1135         String fileUri = uri;
1136         JsonObject item = new JsonObject();
1137         JsonObject options = null;
1138
1139         if (uri.contains(TIMESTAMP_FRAGMENT)) {
1140             fileUri = uri.substring(0, uri.indexOf(TIMESTAMP_FRAGMENT));
1141             String timestamp = uri.substring(uri.indexOf(TIMESTAMP_FRAGMENT) + TIMESTAMP_FRAGMENT.length());
1142             try {
1143                 int s = Integer.parseInt(timestamp);
1144                 options = new JsonObject();
1145                 options.add("resume", timeValueFromSeconds(s));
1146             } catch (NumberFormatException e) {
1147                 logger.warn("Illegal parameter for timestamp - it must be an integer: {}", timestamp);
1148             }
1149         }
1150         item.addProperty("file", fileUri);
1151         playInternal(item, options);
1152     }
1153
1154     public synchronized List<KodiPVRChannelGroup> getPVRChannelGroups(final String pvrChannelType) {
1155         String method = "PVR.GetChannelGroups";
1156         String hash = hostname + '#' + method + "#channeltype=" + pvrChannelType;
1157         JsonElement response = REQUEST_CACHE.putIfAbsentAndGet(hash, () -> {
1158             JsonObject params = new JsonObject();
1159             params.addProperty("channeltype", pvrChannelType);
1160             return socket.callMethod(method, params);
1161         });
1162
1163         List<KodiPVRChannelGroup> pvrChannelGroups = new ArrayList<>();
1164         if (response instanceof JsonObject) {
1165             JsonObject result = response.getAsJsonObject();
1166             if (result.has("channelgroups")) {
1167                 JsonElement channelgroups = result.get("channelgroups");
1168                 if (channelgroups instanceof JsonArray) {
1169                     for (JsonElement element : channelgroups.getAsJsonArray()) {
1170                         JsonObject object = (JsonObject) element;
1171                         KodiPVRChannelGroup pvrChannelGroup = new KodiPVRChannelGroup();
1172                         pvrChannelGroup.setId(object.get("channelgroupid").getAsInt());
1173                         pvrChannelGroup.setLabel(object.get("label").getAsString());
1174                         pvrChannelGroup.setChannelType(pvrChannelType);
1175                         pvrChannelGroups.add(pvrChannelGroup);
1176                     }
1177                 }
1178             }
1179         }
1180         return pvrChannelGroups;
1181     }
1182
1183     public int getPVRChannelGroupId(final String channelType, final String pvrChannelGroupName) {
1184         List<KodiPVRChannelGroup> pvrChannelGroups = getPVRChannelGroups(channelType);
1185         for (KodiPVRChannelGroup pvrChannelGroup : pvrChannelGroups) {
1186             String label = pvrChannelGroup.getLabel();
1187             if (pvrChannelGroupName.equalsIgnoreCase(label)) {
1188                 return pvrChannelGroup.getId();
1189             }
1190         }
1191         // if we don't find a matching PVR channel group return the first (which is the default: "All channels")
1192         return pvrChannelGroups.isEmpty() ? 0 : pvrChannelGroups.get(0).getId();
1193     }
1194
1195     public synchronized List<KodiPVRChannel> getPVRChannels(final int pvrChannelGroupId) {
1196         String method = "PVR.GetChannels";
1197         String hash = hostname + '#' + method + "#channelgroupid=" + pvrChannelGroupId;
1198         JsonElement response = REQUEST_CACHE.putIfAbsentAndGet(hash, () -> {
1199             JsonObject params = new JsonObject();
1200             params.addProperty("channelgroupid", pvrChannelGroupId);
1201             return socket.callMethod(method, params);
1202         });
1203
1204         List<KodiPVRChannel> pvrChannels = new ArrayList<>();
1205         if (response instanceof JsonObject) {
1206             JsonObject result = response.getAsJsonObject();
1207             if (result.has("channels")) {
1208                 JsonElement channels = result.get("channels");
1209                 if (channels instanceof JsonArray) {
1210                     for (JsonElement element : channels.getAsJsonArray()) {
1211                         JsonObject object = (JsonObject) element;
1212                         KodiPVRChannel pvrChannel = new KodiPVRChannel();
1213                         pvrChannel.setId(object.get("channelid").getAsInt());
1214                         pvrChannel.setLabel(object.get("label").getAsString());
1215                         pvrChannel.setChannelGroupId(pvrChannelGroupId);
1216                         pvrChannels.add(pvrChannel);
1217                     }
1218                 }
1219             }
1220         }
1221         return pvrChannels;
1222     }
1223
1224     public int getPVRChannelId(final int pvrChannelGroupId, final String pvrChannelName) {
1225         for (KodiPVRChannel pvrChannel : getPVRChannels(pvrChannelGroupId)) {
1226             String label = pvrChannel.getLabel();
1227             if (pvrChannelName.equalsIgnoreCase(label)) {
1228                 return pvrChannel.getId();
1229             }
1230         }
1231         return 0;
1232     }
1233
1234     public synchronized void playPVRChannel(final int pvrChannelId) {
1235         JsonObject item = new JsonObject();
1236         item.addProperty("channelid", pvrChannelId);
1237
1238         playInternal(item, null);
1239     }
1240
1241     private void playInternal(JsonObject item, JsonObject options) {
1242         JsonObject params = new JsonObject();
1243         params.add("item", item);
1244         if (options != null) {
1245             params.add("options", options);
1246         }
1247         socket.callMethod("Player.Open", params);
1248     }
1249
1250     public synchronized void showNotification(String title, BigDecimal displayTime, String icon, String message) {
1251         JsonObject params = new JsonObject();
1252         params.addProperty("message", message);
1253         if (title != null) {
1254             params.addProperty("title", title);
1255         }
1256         if (displayTime != null) {
1257             params.addProperty("displaytime", displayTime.longValue());
1258         }
1259         if (icon != null) {
1260             params.addProperty("image", callbackUrl + "/icon/" + icon.toLowerCase() + ".png");
1261         }
1262         socket.callMethod("GUI.ShowNotification", params);
1263     }
1264
1265     public boolean checkConnection() {
1266         if (!socket.isConnected()) {
1267             logger.debug("checkConnection: try to connect to Kodi {}", wsUri);
1268             try {
1269                 socket.open();
1270                 return socket.isConnected();
1271             } catch (IOException e) {
1272                 logger.debug("exception during connect to {}", wsUri, e);
1273                 socket.close();
1274                 return false;
1275             }
1276         } else {
1277             // Ping Kodi with the get version command. This prevents the idle timeout on the web socket.
1278             return !getVersion().isEmpty();
1279         }
1280     }
1281
1282     public String getConnectionName() {
1283         return wsUri.toString();
1284     }
1285
1286     public String getVersion() {
1287         if (socket.isConnected()) {
1288             String[] props = { PROPERTY_VERSION };
1289
1290             JsonObject params = new JsonObject();
1291             params.add("properties", getJsonArray(props));
1292             JsonElement response = socket.callMethod("Application.GetProperties", params);
1293
1294             if (response instanceof JsonObject) {
1295                 JsonObject result = response.getAsJsonObject();
1296                 if (result.has(PROPERTY_VERSION)) {
1297                     JsonObject version = result.get(PROPERTY_VERSION).getAsJsonObject();
1298                     int major = version.get("major").getAsInt();
1299                     int minor = version.get("minor").getAsInt();
1300                     String revision = version.get("revision").getAsString();
1301                     return String.format("%d.%d (%s)", major, minor, revision);
1302                 }
1303             }
1304         }
1305         return "";
1306     }
1307
1308     public void input(String key) {
1309         socket.callMethod("Input." + key);
1310     }
1311
1312     public void inputText(String text) {
1313         JsonObject params = new JsonObject();
1314         params.addProperty("text", text);
1315         socket.callMethod("Input.SendText", params);
1316     }
1317
1318     public void inputAction(String action) {
1319         JsonObject params = new JsonObject();
1320         params.addProperty("action", action);
1321         socket.callMethod("Input.ExecuteAction", params);
1322     }
1323
1324     public void inputButtonEvent(String buttonEvent) {
1325         logger.debug("inputButtonEvent {}.", buttonEvent);
1326
1327         String button = buttonEvent;
1328         String keymap = "KB";
1329         Integer holdtime = null;
1330
1331         if (buttonEvent.contains(";")) {
1332             String[] params = buttonEvent.split(";");
1333             switch (params.length) {
1334                 case 2:
1335                     button = params[0];
1336                     keymap = params[1];
1337                     break;
1338                 case 3:
1339                     button = params[0];
1340                     keymap = params[1];
1341                     try {
1342                         holdtime = Integer.parseInt(params[2]);
1343                     } catch (NumberFormatException nfe) {
1344                         holdtime = null;
1345                     }
1346                     break;
1347             }
1348         }
1349
1350         this.inputButtonEvent(button, keymap, holdtime);
1351     }
1352
1353     private void inputButtonEvent(String button, String keymap, Integer holdtime) {
1354         JsonObject params = new JsonObject();
1355         params.addProperty("button", button);
1356         params.addProperty("keymap", keymap);
1357         if (holdtime != null) {
1358             params.addProperty("holdtime", holdtime.intValue());
1359         }
1360         JsonElement result = socket.callMethod("Input.ButtonEvent", params);
1361         logger.debug("inputButtonEvent result {}.", result);
1362     }
1363
1364     public void getSystemProperties() {
1365         KodiSystemProperties systemProperties = null;
1366         if (socket.isConnected()) {
1367             String[] props = { PROPERTY_CANHIBERNATE, PROPERTY_CANREBOOT, PROPERTY_CANSHUTDOWN, PROPERTY_CANSUSPEND };
1368
1369             JsonObject params = new JsonObject();
1370             params.add("properties", getJsonArray(props));
1371             JsonElement response = socket.callMethod("System.GetProperties", params);
1372
1373             try {
1374                 systemProperties = gson.fromJson(response, KodiSystemProperties.class);
1375             } catch (JsonSyntaxException e) {
1376                 // do nothing
1377             }
1378         }
1379         listener.updateSystemProperties(systemProperties);
1380     }
1381
1382     public void sendApplicationQuit() {
1383         String method = "Application.Quit";
1384         socket.callMethod(method);
1385     }
1386
1387     public void sendSystemCommand(String command) {
1388         String method = "System." + command;
1389         socket.callMethod(method);
1390     }
1391
1392     public void profile(String profile) {
1393         JsonObject params = new JsonObject();
1394         params.addProperty("profile", profile);
1395         socket.callMethod("Profiles.LoadProfile", params);
1396     }
1397
1398     public KodiProfile[] getProfiles() {
1399         KodiProfile[] profiles = new KodiProfile[0];
1400         if (socket.isConnected()) {
1401             JsonElement response = socket.callMethod("Profiles.GetProfiles");
1402
1403             try {
1404                 JsonObject profilesJson = response.getAsJsonObject();
1405                 profiles = gson.fromJson(profilesJson.get("profiles"), KodiProfile[].class);
1406             } catch (JsonSyntaxException e) {
1407                 logger.debug("Json syntax exception occurred: {}", e.getMessage(), e);
1408             }
1409         }
1410         return profiles;
1411     }
1412
1413     public void setTime(int time) {
1414         int seconds = time;
1415         JsonObject params = new JsonObject();
1416         params.addProperty("playerid", 1);
1417         JsonObject value = new JsonObject();
1418         JsonObject timeValue = timeValueFromSeconds(seconds);
1419
1420         value.add("time", timeValue);
1421         params.add("value", value);
1422         socket.callMethod("Player.Seek", params);
1423     }
1424
1425     private JsonObject timeValueFromSeconds(int seconds) {
1426         JsonObject timeValue = new JsonObject();
1427         int s = seconds;
1428
1429         if (s >= 3600) {
1430             int hours = s / 3600;
1431             timeValue.addProperty("hours", hours);
1432             s = s % 3600;
1433         }
1434         if (s >= 60) {
1435             int minutes = s / 60;
1436             timeValue.addProperty("minutes", minutes);
1437             s = seconds % 60;
1438         }
1439         timeValue.addProperty("seconds", s);
1440         return timeValue;
1441     }
1442 }