001package org.intellimate.izou.sdk.frameworks.music.user;
002
003import org.intellimate.izou.identification.Identifiable;
004import org.intellimate.izou.identification.Identification;
005import org.intellimate.izou.identification.IdentificationManager;
006import org.intellimate.izou.resource.ResourceModel;
007import org.intellimate.izou.sdk.Context;
008import org.intellimate.izou.sdk.frameworks.music.Capabilities;
009import org.intellimate.izou.sdk.frameworks.music.player.Playlist;
010import org.intellimate.izou.sdk.frameworks.music.player.TrackInfo;
011import org.intellimate.izou.sdk.frameworks.music.player.Volume;
012import org.intellimate.izou.sdk.frameworks.music.resources.*;
013import org.intellimate.izou.sdk.util.AddOnModule;
014
015import java.util.ArrayList;
016import java.util.List;
017import java.util.Optional;
018import java.util.concurrent.CompletableFuture;
019import java.util.concurrent.ExecutionException;
020import java.util.concurrent.TimeUnit;
021import java.util.concurrent.TimeoutException;
022
023/**
024 * this is a simple class which should, added as a resource to an Event, request the Player to play the selected
025 * track or playlist.
026 * <p>
027 * To use this class the player has to meet a few criteria:<br>
028 * <ul>
029 *  <li>the player must exist and be support the standard defined through the sdk</li>
030 *  <li>the players-capabilities must allow requests from outside</li>
031 *  <li>(maybe, depends on the create-method) the players-capabilities must allow a requests with specified a specified playlist/trackInfo</li>
032 * </ul><br>
033 * an example:<br>
034 * <pre>
035 * {@code
036 *
037 * Identification playerID;
038 * Playlist playlist;
039 * List<ResourceModel> resources = PlayerRequest.createPlayerRequest(playlist, player, this) //create a new PlayerRequest
040 *          .map(PlayerRequest::resourcesForExisting) //creates the resources needed to add to the event
041 *          .orElseGet(ArrayList::new);
042 * }
043 * </pre>
044 * @author LeanderK
045 * @version 1.0
046 */
047public class PlayerRequest {
048    private final TrackInfo trackInfo;
049    private final Playlist playlist;
050    private final boolean permanent;
051    private final Identification player;
052    private final Capabilities capabilities;
053    private final Context context;
054    private final Identifiable identifiable;
055    private Volume volume = null;
056
057    /**
058     * internal Constructor
059     * @param trackInfo the trackInfo
060     * @param playlist the playlist
061     * @param permanent whether the Request is permanent
062     * @param player the player
063     * @param capabilities the capabilities
064     * @param context the context
065     * @param identifiable the identifiable
066     */
067    protected PlayerRequest(TrackInfo trackInfo, Playlist playlist, boolean permanent, Identification player,
068                            Capabilities capabilities, Context context, Identifiable identifiable) {
069        this.trackInfo = trackInfo;
070        this.playlist = playlist;
071        this.permanent = permanent;
072        this.player = player;
073        this.capabilities = capabilities;
074        this.context = context;
075        this.identifiable = identifiable;
076    }
077
078    /**
079     * internal Constructor
080     * @param trackInfo the trackInfo
081     * @param playlist the playlist
082     * @param permanent whether the Request is permanent
083     * @param player the player
084     * @param capabilities the capabilities
085     * @param context the Context
086     * @param identifiable the identifiable
087     * @param volume the Volume to set to
088     */
089    protected PlayerRequest(TrackInfo trackInfo, Playlist playlist, boolean permanent, Identification player,
090                            Capabilities capabilities, Context context, Identifiable identifiable, Volume volume) {
091        this.trackInfo = trackInfo;
092        this.playlist = playlist;
093        this.permanent = permanent;
094        this.player = player;
095        this.capabilities = capabilities;
096        this.context = context;
097        this.identifiable = identifiable;
098        this.volume = volume;
099    }
100
101    /**
102     * sets the Volume if the Player supports it.
103     * @param volume the Volume to set to
104     * @return true if the Player supports setting the Volume
105     */
106    public boolean setVolume(Volume volume) {
107        if (capabilities.canChangeVolume()) {
108            this.volume = volume;
109            return true;
110        }
111        return false;
112    }
113
114    /**
115     * tries to set the Volume of the PlayerRequest.
116     * <p>
117     * if the Player supports the Change of the Volume, it will create a new PlayerRequest and return it, if not it
118     * returns this.
119     * </p>
120     * @param volume the Volume to set to
121     * @return a new PlayerRequest or this.
122     */
123    public PlayerRequest trySetVolume(Volume volume) {
124        if (capabilities.canChangeVolume()) {
125            return new PlayerRequest(trackInfo, playlist, permanent, player, capabilities, context, identifiable, volume);
126        }
127        return this;
128    }
129
130    /**
131     * returns a List of Resources that can be added to an already existing event.
132     * <p>
133     * This causes the Addon to block the Event in the OutputPlugin lifecycle of the Event.
134     * @return a List of Resources
135     */
136    @SuppressWarnings("unused")
137    public List<ResourceModel> resourcesForExisting() {
138        List<ResourceModel> resourceModels = new ArrayList<>();
139        IdentificationManager.getInstance().getIdentification(identifiable)
140                .map(id -> new MusicUsageResource(id, true))
141                .ifPresent(resourceModels::add);
142        if (volume != null) {
143            IdentificationManager.getInstance().getIdentification(identifiable)
144                    .map(id -> new VolumeResource(id, volume))
145                    .ifPresent(resourceModels::add);
146        }
147        if (playlist != null) {
148            IdentificationManager.getInstance().getIdentification(identifiable)
149                    .map(id -> new PlaylistResource(id, playlist))
150                    .ifPresent(resourceModels::add);
151        }
152        if (trackInfo != null) {
153            IdentificationManager.getInstance().getIdentification(identifiable)
154                    .map(id -> new TrackInfoResource(id, trackInfo))
155                    .ifPresent(resourceModels::add);
156        }
157        return resourceModels;
158    }
159
160    /**
161     * helper method for PlaylistSelector
162     * @param playlist the playlist selected
163     * @param permanent the permanent addon
164     * @param player the player
165     * @param capabilities the capabilities
166     * @param context the context
167     * @param identifiable the identifiable
168     * @return a PlayerRequest
169     */
170    static PlayerRequest createPlayerRequest(Playlist playlist, boolean permanent, Identification player, Capabilities capabilities, Context context, Identifiable identifiable) {
171        return  new PlayerRequest(null, playlist, permanent, player, capabilities, context, identifiable);
172    }
173
174    /**
175     * creates a new PlayerRequest.
176     * <p>
177     * For this method to return a non-empty Optional the following criteria must be met:<br>
178     * <ul>
179     *  <li>the player must exist and be support the standard defined through the sdk</li>
180     *  <li>the players-capabilities must allow requests from outside</li>
181     * </ul>
182     * @param permanent true means the player can play indefinitely, but only if no one is currently using audio as
183     *                  permanent. It will also not block. false is limited to 10 minutes playback, but will block.
184     * @param player the player to target
185     * @param source the addOnModule used for Context etc.
186     * @return the optional PlayerRequest
187     */
188    @SuppressWarnings("unused")
189    public static Optional<PlayerRequest> createPlayerRequest(boolean permanent, Identification player, AddOnModule source) {
190        if (player == null || source == null)
191            return Optional.empty();
192        try {
193            return source.getContext().getResources()
194                    .generateResource(new CapabilitiesResource(player))
195                    .orElse(CompletableFuture.completedFuture(new ArrayList<>()))
196                    .thenApply(list -> list.stream()
197                                    .filter(resourceModel -> resourceModel.getProvider().equals(player))
198                                    .findAny()
199                                    .flatMap(resource -> Capabilities.importFromResource(resource, source.getContext()))
200                    ).get(1, TimeUnit.SECONDS)
201                    .filter(capabilities -> {
202                        if (!capabilities.handlesPlayRequestFromOutside()) {
203                            source.getContext().getLogger().error("player does not handle play-request from outside");
204                            return false;
205                        }
206                        return true;
207                    })
208                    .map(capabilities -> new PlayerRequest(null, null, permanent, player, capabilities, source.getContext(), source));
209        } catch (InterruptedException | ExecutionException | TimeoutException e) {
210            source.getContext().getLogger().error("unable to obtain capabilities");
211            return Optional.empty();
212        }
213    }
214
215    /**
216     * creates a new PlayerRequest.
217     * <p>
218     * the resulting PlayerRequest is not permanent, which means that it will mute all other sound but is limited to
219     * 10 minutes.<br>
220     * For this method to return a non-empty Optional the following criteria must be met:<br>
221     * <ul>
222     *  <li>the player must exist and be support the standard defined through the sdk</li>
223     *  <li>the players-capabilities must allow requests from outside</li>
224     *  <li>the players-capabilities must allow a requests with specified a specified playlist/trackInfo</li>
225     * </ul>
226     * @param trackInfo the trackInfo to pass with the request
227     * @param player the player to target
228     * @param source the addOnModule used for Context etc.
229     * @return the optional PlayerRequest
230     */
231    @SuppressWarnings("unused")
232    public static Optional<PlayerRequest> createPlayerRequest(TrackInfo trackInfo, Identification player, AddOnModule source) {
233        return createPlayerRequest(trackInfo, false, player, source);
234    }
235
236    /**
237     * creates a new PlayerRequest.
238     * <p>
239     * For this method to return a non-empty Optional the following criteria must be met:<br>
240     * <ul>
241     *  <li>the player must exist and be support the standard defined through the sdk</li>
242     *  <li>the players-capabilities must allow requests from outside</li>
243     *  <li>the players-capabilities must allow a requests with specified a specified playlist/trackInfo</li>
244     * </ul>
245     * @param trackInfo the trackInfo to pass with the request
246     * @param permanent true means the player can play indefinitely, but only if no one is currently using audio as
247     *                  permanent. It will also not block. false is limited to 10 minutes playback, but will block.
248     * @param player the player to target
249     * @param source the addOnModule used for Context etc.
250     * @return the optional PlayerRequest
251     */
252    @SuppressWarnings("unused")
253    public static Optional<PlayerRequest> createPlayerRequest(TrackInfo trackInfo, boolean permanent, Identification player, AddOnModule source) {
254        if (trackInfo == null ||player == null || source == null)
255            return Optional.empty();
256        try {
257            return source.getContext().getResources()
258                    .generateResource(new CapabilitiesResource(player))
259                    .orElse(CompletableFuture.completedFuture(new ArrayList<>()))
260                    .thenApply(list -> list.stream()
261                                    .filter(resourceModel -> resourceModel.getProvider().equals(player))
262                                    .findAny()
263                                    .flatMap(resource -> Capabilities.importFromResource(resource, source.getContext()))
264                    ).get(1, TimeUnit.SECONDS)
265                    .filter(capabilities -> {
266                        if (!capabilities.handlesPlayRequestFromOutside()) {
267                            source.getContext().getLogger().error("player does not handle play-request from outside");
268                            return false;
269                        }
270                        if (!capabilities.hasPlayRequestDetailed()) {
271                            source.getContext().getLogger().error("player does not handle trackInfo-request from outside");
272                            return false;
273                        }
274                        return true;
275                    })
276                    .map(capabilities -> new PlayerRequest(trackInfo, null, permanent, player, capabilities, source.getContext(), source));
277        } catch (InterruptedException | ExecutionException | TimeoutException e) {
278            source.getContext().getLogger().error("unable to obtain capabilities");
279            return Optional.empty();
280        }
281    }
282
283    /**
284     * creates a new PlayerRequest.
285     * <p>
286     * the resulting PlayerRequest is not permanent, which means that it will mute all other sound but is limited to
287     * 10 minutes.<br>
288     * For this method to return a non-empty Optional the following criteria must be met:<br>
289     * <ul>
290     *  <li>the player must exist and be support the standard defined through the sdk</li>
291     *  <li>the players-capabilities must allow requests from outside</li>
292     *  <li>the players-capabilities must allow a requests with specified a specified playlist/trackInfo</li>
293     * </ul>
294     * @param playlist the playlist to pass with the request
295     * @param player the player to target
296     * @param source the addOnModule used for Context etc.
297     * @return the optional PlayerRequest
298     */
299    @SuppressWarnings("unused")
300    public static Optional<PlayerRequest> createPlayerRequest(Playlist playlist, Identification player, AddOnModule source) {
301        return createPlayerRequest(playlist, false, player, source);
302    }
303
304    /**
305     * creates a new PlayerRequest.
306     * <p>
307     * For this method to return a non-empty Optional the following criteria must be met:<br>
308     * <ul>
309     *  <li>the player must exist and be support the standard defined through the sdk</li>
310     *  <li>the players-capabilities must allow requests from outside</li>
311     *  <li>the players-capabilities must allow a requests with specified a specified playlist/trackInfo</li>
312     * </ul>
313     * @param playlist the playlist to pass with the request
314     * @param permanent true means the player can play indefinitely, but only if no one is currently using audio as
315     *                  permanent. It will also not block. false is limited to 10 minutes playback, but will block.
316     * @param player the player to target
317     * @param source the addOnModule used for Context etc.
318     * @return the optional PlayerRequest
319     */
320    @SuppressWarnings("unused")
321    public static Optional<PlayerRequest> createPlayerRequest(Playlist playlist, boolean permanent, Identification player, AddOnModule source) {
322        if (playlist == null ||player == null || source == null)
323            return Optional.empty();
324        try {
325            return source.getContext().getResources()
326                    .generateResource(new CapabilitiesResource(player))
327                    .orElse(CompletableFuture.completedFuture(new ArrayList<>()))
328                    .thenApply(list -> list.stream()
329                                    .filter(resourceModel -> resourceModel.getProvider().equals(player))
330                                    .findAny()
331                                    .flatMap(resource -> Capabilities.importFromResource(resource, source.getContext()))
332                    ).get(1, TimeUnit.SECONDS)
333                    .filter(capabilities -> {
334                        if (!capabilities.handlesPlayRequestFromOutside()) {
335                            source.getContext().getLogger().error("player does not handle play-request from outside");
336                            return false;
337                        }
338                        if (!capabilities.hasPlayRequestDetailed()) {
339                            source.getContext().getLogger().error("player does not handle playlist-request from outside");
340                            return false;
341                        }
342                        if (!playlist.verify(capabilities)) {
343                            source.getContext().getLogger().error("player can not handle the playlist, probably illegal PlaybackModes");
344                            return false;
345                        }
346                        return true;
347                    })
348                    .map(capabilities -> new PlayerRequest(null, playlist, permanent, player, capabilities, source.getContext(), source));
349        } catch (InterruptedException | ExecutionException | TimeoutException e) {
350            source.getContext().getLogger().debug("unable to obtain capabilities");
351            return Optional.empty();
352        }
353    }
354}