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