2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.upnpcontrol.internal.queue;
15 import static org.openhab.binding.upnpcontrol.internal.UpnpControlBindingConstants.PLAYLIST_FILE_EXTENSION;
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;
26 import java.util.Map.Entry;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import com.google.gson.Gson;
36 import com.google.gson.JsonParseException;
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.
45 * @author Mark Herwege - Initial contribution
49 public class UpnpEntryQueue {
51 private final Logger logger = LoggerFactory.getLogger(UpnpEntryQueue.class);
53 private volatile boolean repeat = false;
54 private volatile boolean shuffle = false;
56 private volatile int currentIndex = -1;
58 private class Playlist {
59 @SuppressWarnings("unused")
60 String name; // Used in serialization
61 volatile Map<String, List<UpnpEntry>> masterList;
63 Playlist(String name, Map<String, List<UpnpEntry>> masterList) {
65 this.masterList = masterList;
69 private volatile Playlist playlist;
71 private volatile List<UpnpEntry> currentQueue;
72 private volatile List<UpnpEntry> shuffledQueue = Collections.emptyList();
74 private final Gson gson = new Gson();
76 public UpnpEntryQueue() {
77 this(Collections.emptyList());
83 public UpnpEntryQueue(List<UpnpEntry> 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.
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());
102 * Switch on/off repeat mode.
106 public void setRepeat(boolean repeat) {
107 this.repeat = repeat;
111 * Switch on/off shuffle mode.
115 public synchronized void setShuffle(boolean shuffle) {
119 int index = currentIndex;
121 currentIndex = currentQueue.indexOf(shuffledQueue.get(index));
123 this.shuffle = false;
127 private synchronized void shuffle() {
128 UpnpEntry current = null;
129 int index = currentIndex;
131 current = this.shuffle ? shuffledQueue.get(index) : currentQueue.get(index);
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);
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.
152 public synchronized @Nullable UpnpEntry next() {
154 if (currentIndex >= size()) {
155 if (shuffle && repeat) {
159 currentIndex = repeat ? 0 : -1;
161 return currentIndex >= 0 ? get(currentIndex) : null;
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.
169 public synchronized @Nullable UpnpEntry previous() {
171 if (currentIndex < 0) {
172 if (shuffle && repeat) {
176 currentIndex = repeat ? (size() - 1) : -1;
178 return currentIndex >= 0 ? get(currentIndex) : null;
182 * @return the index of the current element in the queue.
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.
192 public synchronized int nextIndex() {
193 int index = currentIndex + 1;
194 if (index >= size()) {
195 index = repeat ? 0 : -1;
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.
204 public synchronized int previousIndex() {
205 int index = currentIndex - 1;
207 index = repeat ? (size() - 1) : -1;
213 * @return true if there is an element to server when calling {@link #next}.
215 public synchronized boolean hasNext() {
216 int size = currentQueue.size();
217 if (repeat && (size > 0)) {
220 return (currentIndex < (size - 1));
224 * @return true if there is an element to server when calling {@link #previous}.
226 public synchronized boolean hasPrevious() {
227 int size = currentQueue.size();
228 if (repeat && (size > 0)) {
231 return (currentIndex > 0);
236 * @return the UpnpEntry at the index position in the queue, or null when none can be retrieved.
238 public @Nullable synchronized UpnpEntry get(int index) {
239 if ((index >= 0) && (index < size())) {
241 return shuffledQueue.get(index);
243 return currentQueue.get(index);
251 * Reset the queue position to before the start of the queue (-1).
253 public synchronized void resetIndex() {
261 * @return number of element in the queue.
263 public synchronized int size() {
264 return currentQueue.size();
268 * @return true if the queue is empty.
270 public synchronized boolean isEmpty() {
271 return currentQueue.isEmpty();
275 * Persist queue as a playlist with name "current"
277 * @param path of playlist directory
279 public void persistQueue(String path) {
280 persistQueue("current", false, path);
284 * Persist the queue as a playlist.
286 * @param name of the playlist
287 * @param append to the playlist if it already exists
288 * @param path of playlist directory
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);
297 // ensure full path exists
298 file.getParentFile().mkdirs();
300 if (append && file.exists()) {
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);
311 // Merging masterList with persistList, overwriting persistList UpnpEntry objects with same id
312 playlist.masterList.forEach((u, list) -> appendList.masterList.merge(u, list,
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))
319 json = gson.toJson(new Playlist(name, appendList.masterList));
321 } catch (JsonParseException | UnsupportedOperationException e) {
322 logger.debug("Could not append, JsonParseException reading {}: {}", file.toPath(), e.getMessage(),
325 } catch (IOException e) {
326 logger.debug("Could not append, IOException reading playlist {} from {}", name, file.toPath());
330 playlist.name = name;
331 json = gson.toJson(playlist);
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());
342 * Replace the current queue with the playlist name and reset the queue index.
345 * @param path directory containing playlist to restore
347 public void restoreQueue(String name, @Nullable String path) {
348 restoreQueue(name, null, path);
352 * Replace the current queue with the playlist name and reset the queue index. Filter the content of the playlist on
356 * @param udn of the server the playlist entries were created on, all entries when null
357 * @param path of playlist directory
359 public synchronized void restoreQueue(String name, @Nullable String udn, @Nullable String path) {
364 String fileName = path + name + PLAYLIST_FILE_EXTENSION;
365 File file = new File(fileName);
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);
373 Playlist list = gson.fromJson(json, Playlist.class);
375 logger.debug("Empty playlist file {}", file.getAbsolutePath());
381 Stream<Entry<String, List<UpnpEntry>>> stream = playlist.masterList.entrySet().stream();
383 stream = stream.filter(u -> u.getKey().equals(udn));
385 currentQueue = stream.map(p -> p.getValue()).flatMap(List::stream).filter(e -> !e.isContainer())
386 .collect(Collectors.toList());
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());
397 * @return list of all UpnpEntries in the queue.
399 public List<UpnpEntry> getEntryList() {