]> git.basschouten.com Git - openhab-addons.git/blob
2efbed0c0c0a1812b8306db1c1e72a0f5aee68ac
[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.upnpcontrol.internal.queue;
14
15 import static org.openhab.binding.upnpcontrol.internal.UpnpControlBindingConstants.PLAYLIST_FILE_EXTENSION;
16
17 import java.io.File;
18 import java.io.IOException;
19 import java.nio.charset.StandardCharsets;
20 import java.nio.file.Files;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.google.gson.Gson;
36 import com.google.gson.JsonParseException;
37
38 /**
39  * The class {@link UpnpEntryQueue} represents a queue of UPnP media entries to be played on a renderer. It keeps track
40  * of a current index in the queue. It has convenience methods to play previous/next entries, whereby the queue can be
41  * organized to play from first to last (with no repetition), to restart at the start when the end is reached (in a
42  * continuous loop), or to random shuffle the entries. Repeat and shuffle are off by default, but can be set using the
43  * {@link #setRepeat} and {@link #setShuffle} methods.
44  *
45  * @author Mark Herwege - Initial contribution
46  *
47  */
48 @NonNullByDefault
49 public class UpnpEntryQueue {
50
51     private final Logger logger = LoggerFactory.getLogger(UpnpEntryQueue.class);
52
53     private volatile boolean repeat = false;
54     private volatile boolean shuffle = false;
55
56     private volatile int currentIndex = -1;
57
58     private class Playlist {
59         @SuppressWarnings("unused")
60         String name; // Used in serialization
61         volatile Map<String, List<UpnpEntry>> masterList;
62
63         Playlist(String name, Map<String, List<UpnpEntry>> masterList) {
64             this.name = name;
65             this.masterList = masterList;
66         }
67     }
68
69     private volatile Playlist playlist;
70
71     private volatile List<UpnpEntry> currentQueue;
72     private volatile List<UpnpEntry> shuffledQueue = Collections.emptyList();
73
74     private final Gson gson = new Gson();
75
76     public UpnpEntryQueue() {
77         this(Collections.emptyList());
78     }
79
80     /**
81      * @param queue
82      */
83     public UpnpEntryQueue(List<UpnpEntry> queue) {
84         this(queue, "");
85     }
86
87     /**
88      * @param queue
89      * @param udn Defines the UPnP media server source of the queue, could be used to re-query the server if URL
90      *            resources are out of date.
91      */
92     public UpnpEntryQueue(List<UpnpEntry> queue, @Nullable String udn) {
93         String serverUdn = (udn != null) ? udn : "";
94         Map<String, List<UpnpEntry>> masterList = Collections.synchronizedMap(new HashMap<>());
95         List<UpnpEntry> localQueue = new ArrayList<>(queue);
96         masterList.put(serverUdn, localQueue);
97         playlist = new Playlist("", masterList);
98         currentQueue = localQueue.stream().filter(e -> !e.isContainer()).collect(Collectors.toList());
99     }
100
101     /**
102      * Switch on/off repeat mode.
103      *
104      * @param repeat
105      */
106     public void setRepeat(boolean repeat) {
107         this.repeat = repeat;
108     }
109
110     /**
111      * Switch on/off shuffle mode.
112      *
113      * @param shuffle
114      */
115     public synchronized void setShuffle(boolean shuffle) {
116         if (shuffle) {
117             shuffle();
118         } else {
119             int index = currentIndex;
120             if (index != -1) {
121                 currentIndex = currentQueue.indexOf(shuffledQueue.get(index));
122             }
123             this.shuffle = false;
124         }
125     }
126
127     private synchronized void shuffle() {
128         UpnpEntry current = null;
129         int index = currentIndex;
130         if (index != -1) {
131             current = this.shuffle ? shuffledQueue.get(index) : currentQueue.get(index);
132         }
133
134         // Shuffle the queue again
135         shuffledQueue = new ArrayList<UpnpEntry>(currentQueue);
136         Collections.shuffle(shuffledQueue);
137         if (current != null) {
138             // Put the current entry at the beginning of the shuffled queue
139             shuffledQueue.remove(current);
140             shuffledQueue.add(0, current);
141             currentIndex = 0;
142         }
143
144         this.shuffle = true;
145     }
146
147     /**
148      * @return will return the next element in the queue, or null when the end of the queue has been reached. With
149      *         repeat set, will restart at the beginning of the queue when the end has been reached. The method will
150      *         return null if the queue is empty.
151      */
152     public synchronized @Nullable UpnpEntry next() {
153         currentIndex++;
154         if (currentIndex >= size()) {
155             if (shuffle && repeat) {
156                 currentIndex = -1;
157                 shuffle();
158             }
159             currentIndex = repeat ? 0 : -1;
160         }
161         return currentIndex >= 0 ? get(currentIndex) : null;
162     }
163
164     /**
165      * @return will return the previous element in the queue, or null when the start of the queue has been reached. With
166      *         repeat set, will restart at the end of the queue when the start has been reached. The method will return
167      *         null if the queue is empty.
168      */
169     public synchronized @Nullable UpnpEntry previous() {
170         currentIndex--;
171         if (currentIndex < 0) {
172             if (shuffle && repeat) {
173                 currentIndex = -1;
174                 shuffle();
175             }
176             currentIndex = repeat ? (size() - 1) : -1;
177         }
178         return currentIndex >= 0 ? get(currentIndex) : null;
179     }
180
181     /**
182      * @return the index of the current element in the queue.
183      */
184     public int index() {
185         return currentIndex;
186     }
187
188     /**
189      * @return the index of the next element in the queue that will be served if {@link #next} is called, or -1 if
190      *         nothing to serve for next.
191      */
192     public synchronized int nextIndex() {
193         int index = currentIndex + 1;
194         if (index >= size()) {
195             index = repeat ? 0 : -1;
196         }
197         return index;
198     }
199
200     /**
201      * @return the index of the previous element in the queue that will be served if {@link #previous} is called, or -1
202      *         if nothing to serve for next.
203      */
204     public synchronized int previousIndex() {
205         int index = currentIndex - 1;
206         if (index < 0) {
207             index = repeat ? (size() - 1) : -1;
208         }
209         return index;
210     }
211
212     /**
213      * @return true if there is an element to server when calling {@link #next}.
214      */
215     public synchronized boolean hasNext() {
216         int size = currentQueue.size();
217         if (repeat && (size > 0)) {
218             return true;
219         }
220         return (currentIndex < (size - 1));
221     }
222
223     /**
224      * @return true if there is an element to server when calling {@link #previous}.
225      */
226     public synchronized boolean hasPrevious() {
227         int size = currentQueue.size();
228         if (repeat && (size > 0)) {
229             return true;
230         }
231         return (currentIndex > 0);
232     }
233
234     /**
235      * @param index
236      * @return the UpnpEntry at the index position in the queue, or null when none can be retrieved.
237      */
238     public @Nullable synchronized UpnpEntry get(int index) {
239         if ((index >= 0) && (index < size())) {
240             if (shuffle) {
241                 return shuffledQueue.get(index);
242             } else {
243                 return currentQueue.get(index);
244             }
245         } else {
246             return null;
247         }
248     }
249
250     /**
251      * Reset the queue position to before the start of the queue (-1).
252      */
253     public synchronized void resetIndex() {
254         currentIndex = -1;
255         if (shuffle) {
256             shuffle();
257         }
258     }
259
260     /**
261      * @return number of element in the queue.
262      */
263     public synchronized int size() {
264         return currentQueue.size();
265     }
266
267     /**
268      * @return true if the queue is empty.
269      */
270     public synchronized boolean isEmpty() {
271         return currentQueue.isEmpty();
272     }
273
274     /**
275      * Persist queue as a playlist with name "current"
276      *
277      * @param path of playlist directory
278      */
279     public void persistQueue(String path) {
280         persistQueue("current", false, path);
281     }
282
283     /**
284      * Persist the queue as a playlist.
285      *
286      * @param name of the playlist
287      * @param append to the playlist if it already exists
288      * @param path of playlist directory
289      */
290     public synchronized void persistQueue(String name, boolean append, String path) {
291         String fileName = path + name + PLAYLIST_FILE_EXTENSION;
292         File file = new File(fileName);
293
294         String json;
295
296         try {
297             // ensure full path exists
298             file.getParentFile().mkdirs();
299
300             if (append && file.exists()) {
301                 try {
302                     logger.debug("Reading contents of {} for appending", file.getAbsolutePath());
303                     final byte[] contents = Files.readAllBytes(file.toPath());
304                     json = new String(contents, StandardCharsets.UTF_8);
305                     Playlist appendList = gson.fromJson(json, Playlist.class);
306                     if (appendList == null) {
307                         // empty playlist file, so just overwrite
308                         playlist.name = name;
309                         json = gson.toJson(playlist);
310                     } else {
311                         // Merging masterList with persistList, overwriting persistList UpnpEntry objects with same id
312                         playlist.masterList.forEach((u, list) -> appendList.masterList.merge(u, list,
313                                 (oldlist,
314                                         newlist) -> new ArrayList<>(Stream.of(oldlist, newlist).flatMap(List::stream)
315                                                 .collect(Collectors.toMap(UpnpEntry::getId, entry -> entry,
316                                                         (UpnpEntry oldentry, UpnpEntry newentry) -> newentry))
317                                                 .values())));
318
319                         json = gson.toJson(new Playlist(name, appendList.masterList));
320                     }
321                 } catch (JsonParseException | UnsupportedOperationException e) {
322                     logger.debug("Could not append, JsonParseException reading {}: {}", file.toPath(), e.getMessage(),
323                             e);
324                     return;
325                 } catch (IOException e) {
326                     logger.debug("Could not append, IOException reading playlist {} from {}", name, file.toPath());
327                     return;
328                 }
329             } else {
330                 playlist.name = name;
331                 json = gson.toJson(playlist);
332             }
333
334             final byte[] contents = json.getBytes(StandardCharsets.UTF_8);
335             Files.write(file.toPath(), contents);
336         } catch (IOException e) {
337             logger.debug("IOException writing playlist {} to {}", name, file.toPath());
338         }
339     }
340
341     /**
342      * Replace the current queue with the playlist name and reset the queue index.
343      *
344      * @param name
345      * @param path directory containing playlist to restore
346      */
347     public void restoreQueue(String name, @Nullable String path) {
348         restoreQueue(name, null, path);
349     }
350
351     /**
352      * Replace the current queue with the playlist name and reset the queue index. Filter the content of the playlist on
353      * the server udn.
354      *
355      * @param name
356      * @param udn of the server the playlist entries were created on, all entries when null
357      * @param path of playlist directory
358      */
359     public synchronized void restoreQueue(String name, @Nullable String udn, @Nullable String path) {
360         if (path == null) {
361             return;
362         }
363
364         String fileName = path + name + PLAYLIST_FILE_EXTENSION;
365         File file = new File(fileName);
366
367         if (file.exists()) {
368             try {
369                 logger.debug("Reading contents of {}", file.getAbsolutePath());
370                 final byte[] contents = Files.readAllBytes(file.toPath());
371                 final String json = new String(contents, StandardCharsets.UTF_8);
372
373                 Playlist list = gson.fromJson(json, Playlist.class);
374                 if (list == null) {
375                     logger.debug("Empty playlist file {}", file.getAbsolutePath());
376                     return;
377                 }
378
379                 playlist = list;
380
381                 Stream<Entry<String, List<UpnpEntry>>> stream = playlist.masterList.entrySet().stream();
382                 if (udn != null) {
383                     stream = stream.filter(u -> u.getKey().equals(udn));
384                 }
385                 currentQueue = stream.map(p -> p.getValue()).flatMap(List::stream).filter(e -> !e.isContainer())
386                         .collect(Collectors.toList());
387                 resetIndex();
388             } catch (JsonParseException | UnsupportedOperationException e) {
389                 logger.debug("JsonParseException reading {}: {}", file.toPath(), e.getMessage(), e);
390             } catch (IOException e) {
391                 logger.debug("IOException reading playlist {} from {}", name, file.toPath());
392             }
393         }
394     }
395
396     /**
397      * @return list of all UpnpEntries in the queue.
398      */
399     public List<UpnpEntry> getEntryList() {
400         return currentQueue;
401     }
402 }