All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.deepsymmetry.beatlink.data.MenuLoader Maven / Gradle / Ivy

There is a newer version: 7.4.0
Show newest version
package org.deepsymmetry.beatlink.data;

import org.deepsymmetry.beatlink.CdjStatus;
import org.deepsymmetry.beatlink.MediaDetails;
import org.deepsymmetry.beatlink.dbserver.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Provides support for navigating the menu hierarchy offered by the dbserver on a player for a particular media slot.
 * Note that for historical reasons, loading track metadata, playlists, and the full track list are performed by the
 * {@link MetadataFinder}, even though those are technically menu operations.
 *
 * @since 0.4.0
 *
 * @author James Elliott
 */
public class MenuLoader {

    private static final Logger logger = LoggerFactory.getLogger(MenuLoader.class);

    /**
     * Ask the specified player for its top-level menu of menus. The {@link MetadataFinder} must be running for us to
     * know the right kind of message to send, because it depends on whether the slot holds a rekordbox database or not.
     * If we can't tell (because it's not running), we will just guess that there is one, and perhaps get back nothing.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details, although it does not seem to have an effect on the root menu
     *
     * @return the entries in the top level menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestRootMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        final MediaDetails details = MetadataFinder.getInstance().getMediaDetailsFor(slotReference);
                        final CdjStatus.TrackType mediaType = details == null? CdjStatus.TrackType.REKORDBOX : details.mediaType;

                        final Message response = client.menuRequestTyped(Message.KnownType.ROOT_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                mediaType, new NumberField(sortOrder), new NumberField(0xffffff));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, mediaType, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting root menu");
    }

    /**
     * Ask the specified player for a Playlist menu. This boils down to a call to
     * {@link MetadataFinder#requestPlaylistItemsFrom(int, CdjStatus.TrackSourceSlot, int, int, boolean)} asking for
     * the playlist folder with ID 0, but it is also made available here since this is likely where people will be
     * looking for the capability. To get the contents of individual playlists or sub-folders, pass the playlist or
     * folder ID obtained by calling this to that function.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the playlists and folders in the playlist menu
     *
     * @see MetadataFinder#requestPlaylistItemsFrom(int, CdjStatus.TrackSourceSlot, int, int, boolean)
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestPlaylistMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        return MetadataFinder.getInstance().requestPlaylistItemsFrom(slotReference.player, slotReference.slot, sortOrder,
                0, true);
    }

    /**
     * Ask the specified player for a History menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details, although it does not seem to have an effect on the history menu
     *
     * @return the entries in the history menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestHistoryMenuFrom(final SlotReference slotReference, final int sortOrder)
        throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting History menu.");
                        Message response = client.menuRequest(Message.KnownType.HISTORY_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting history menu");
    }

    /**
     * Ask the specified player a History playlist.
     *
     * @param slotReference the player and slot for which the playlist is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param historyId identifies which history session's playlist is desired
     *
     * @return the entries in the history playlist
     *
     * @throws Exception if there is a problem obtaining the playlist
     */
    public List requestHistoryPlaylistFrom(final SlotReference slotReference, final int sortOrder, final int historyId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting History playlist.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_HISTORY_REQ, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(historyId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting history playlist");
    }

    /**
     * Ask the specified player for a Track menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the track menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestTrackMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                return MetadataFinder.getInstance().getFullTrackList(slotReference.slot, client, sortOrder);
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting track menu");
    }

    /**
     * Ask the specified player for an Artist menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the artist menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestArtistMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Artist menu.");
                        Message response = client.menuRequest(Message.KnownType.ARTIST_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting artist menu");
    }

    /**
     * Ask the specified player for an Artist Album menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param artistId  the artist whose album menu is desired
     *
     * @return the entries in the artist album menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestArtistAlbumMenuFrom(final SlotReference slotReference, final int sortOrder, final int artistId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Artist Album menu.");
                        Message response = client.menuRequest(Message.KnownType.ALBUM_MENU_FOR_ARTIST_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder), new NumberField(artistId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting artist album menu");
    }

    /**
     * Ask the specified player for an Artist Album Tracks menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param artistId the artist whose album track menu is desired
     * @param albumId the album whose track menu is desired, or -1 for all albums
     *
     * @return the entries in the artist albums menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestArtistAlbumTrackMenuFrom(final SlotReference slotReference, final int sortOrder, final int artistId, final int albumId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Artist Album Track menu.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_ARTIST_AND_ALBUM, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(artistId), new NumberField(albumId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting artist album tracks menu");
    }

    /**
     * Ask the specified player for an Album Track menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param albumId the album whose track menu is desired
     *
     * @return the entries in the album track menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestAlbumTrackMenuFrom(final SlotReference slotReference, final int sortOrder, final int albumId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Album Track menu.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_ALBUM_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder), new NumberField(albumId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting album tracks menu");
    }

    /**
     * Ask the specified player for a Genre menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the genre menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestGenreMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Genre menu.");
                        Message response = client.menuRequest(Message.KnownType.GENRE_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting genre menu");
    }

    /**
     * Ask the specified player for a Genre Artists menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param genreId the genre whose artist menu is desired
     *
     * @return the entries in the genre artists menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestGenreArtistMenuFrom(final SlotReference slotReference, final int sortOrder, final int genreId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Genre Artist menu.");
                        Message response = client.menuRequest(Message.KnownType.ARTIST_MENU_FOR_GENRE_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder), new NumberField(genreId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting genre artists menu");
    }

    /**
     * Ask the specified player for a Genre Artist Albums menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param genreId the genre whose artist album menu is desired
     * @param artistId the artist whose album menu is desired, or -1 for all artists
     *
     * @return the entries in the genre artist albums menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestGenreArtistAlbumMenuFrom(final SlotReference slotReference, final int sortOrder, final int genreId, final int artistId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Genre Artist Album menu.");
                        Message response = client.menuRequest(Message.KnownType.ALBUM_MENU_FOR_GENRE_AND_ARTIST, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(genreId), new NumberField(artistId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting genre artist albums menu");
    }

    /**
     * Ask the specified player for a Genre Artist Album Tracks menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param genreId the genre whose artist album track menu is desired
     * @param artistId the artist whose album track menu is desired, or -1 for all artists
     * @param albumId the album whose track menu is desired, or -1 for all albums
     *
     * @return the entries in the genre artist albums menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestGenreArtistAlbumTrackMenuFrom(final SlotReference slotReference, final int sortOrder, final int genreId,
                                                              final int artistId, final int albumId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Genre Artist Album Track menu.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_GENRE_ARTIST_AND_ALBUM, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(genreId), new NumberField(artistId), new NumberField(albumId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting genre artist album tracks menu");
    }

    /**
     * Ask the specified player for an Album menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the album menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestAlbumMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Album menu.");
                        Message response = client.menuRequest(Message.KnownType.ALBUM_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting album menu");
    }

    /**
     * Ask the specified player for a Key menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the key menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestKeyMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Key menu.");
                        Message response = client.menuRequest(Message.KnownType.KEY_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting key menu");
    }

    /**
     * Ask the specified player for a key neighbor menu for a given key.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param keyId the key whose available compatible keys are desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the key neighbor menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestKeyNeighborMenuFrom(final SlotReference slotReference, final int sortOrder, final int keyId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting key neighbor menu.");
                        Message response = client.menuRequest(Message.KnownType.NEIGHBOR_MENU_FOR_KEY, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(keyId));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting key neighbor menu");
    }

    /**
     * Ask the specified player for a track menu for an allowed distance from a given key.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param keyId the key whose compatible tracks are desired
     * @param distance how far along the circle of fifths are the tracks allowed to be from the specified key
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the matching tracks
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestTracksByKeyAndDistanceFrom(final SlotReference slotReference, final int sortOrder, final int keyId, final int distance)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting key neighbor menu.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_KEY_AND_DISTANCE, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(keyId), new NumberField(distance));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting tracks by key and distance menu");
    }

    /**
     * Ask the specified player for a BPM menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the BPM menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestBpmMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting BPM menu.");
                        Message response = client.menuRequest(Message.KnownType.BPM_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting BPM menu");
    }

    /**
     * Ask the specified player for a tempo range menu for a given BPM.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param bpm the tempo whose nearby ranges are desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the tempo range menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestBpmRangeMenuFrom(final SlotReference slotReference, final int sortOrder, final int bpm)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting tempo neighbor menu.");
                        Message response = client.menuRequest(Message.KnownType.BPM_RANGE_REQ, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(bpm));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting tempo range menu");
    }

    /**
     * Ask the specified player for tracks whose tempo falls within a specific percentage of a given BPM.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param bpm the tempo that tracks must be close to
     * @param range the percentage by which the actual tempo may differ for a track to still be returned
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the tracks whose tempo falls within the specified range
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestTracksByBpmRangeFrom(final SlotReference slotReference, final int sortOrder, final int bpm, final int range)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting tempo neighbor menu.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_BPM_AND_DISTANCE, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(bpm), new NumberField(range));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting tracks within tempo range menu");
    }

    /**
     * Ask the specified player for a Rating menu.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the entries in the rating menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestRatingMenuFrom(final SlotReference slotReference, final int sortOrder)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Rating menu.");
                        Message response = client.menuRequest(Message.KnownType.RATING_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                new NumberField(sortOrder));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting rating menu");
    }

    /**
     * Ask the specified player for a track menu for a given rating.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param rating the desired rating for tracks to be returned
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     *
     * @return the matching tracks
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestTracksByRatingFrom(final SlotReference slotReference, final int sortOrder, final int rating)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting key neighbor menu.");
                        Message response = client.menuRequest(Message.KnownType.TRACK_MENU_FOR_RATING_REQ, Message.MenuIdentifier.MAIN_MENU,
                                slotReference.slot, new NumberField(sortOrder), new NumberField(rating));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.REKORDBOX, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting tracks by rating menu");
    }


    /**
     * Ask the specified player for a Folder menu for exploring its raw filesystem.
     * This is a request for unanalyzed items, so we do a typed menu request.
     *
     * @param slotReference the player and slot for which the menu is desired
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details
     * @param folderId identifies the folder whose contents should be listed, use -1 to get the root folder
     *
     * @return the entries in the folder menu
     *
     * @throws Exception if there is a problem obtaining the menu
     */
    public List requestFolderMenuFrom(final SlotReference slotReference, final int sortOrder, final int folderId)
            throws Exception {

        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
                    try {
                        logger.debug("Requesting Key menu.");
                        Message response = client.menuRequestTyped(Message.KnownType.FOLDER_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slotReference.slot,
                                CdjStatus.TrackType.UNANALYZED, new NumberField(sortOrder), new NumberField(folderId), new NumberField(0xffffff));
                        return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slotReference.slot, CdjStatus.TrackType.UNANALYZED, response);
                    } finally {
                        client.unlockForMenuOperations();
                    }
                } else {
                    throw new TimeoutException("Unable to lock player for menu operations.");
                }
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(slotReference.player, task, "requesting folder menu");
    }

    /**
     * Ask the connected dbserver about database records whose names contain {@code text}. If {@code count} is not
     * {@code null}, no more than that many results will be returned, and the value will be set to the total number
     * of results that were available. Otherwise all results will be returned.
     *
     * @param slot the slot in which the database can be found
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details, although it does not seem to have an effect on searches.
     * @param text the search text used to filter the results
     * @param count if present, sets an upper limit on the number of results to return, and will get set
     *              to the actual number that were available
     *
     * @return the items that match the specified search string; they may be a variety of different types
     *
     * @throws IOException if there is a problem communicating
     * @throws InterruptedException if the thread is interrupted while trying to lock the client for menu operations
     * @throws TimeoutException if we are unable to lock the client for menu operations
     */
    private List getSearchItems(CdjStatus.TrackSourceSlot slot, int sortOrder, String text,
                                         AtomicInteger count, Client client)
            throws IOException, InterruptedException, TimeoutException {
        if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
            try {
                final StringField textField = new StringField(text);
                Message response = client.menuRequest(Message.KnownType.SEARCH_MENU, Message.MenuIdentifier.MAIN_MENU, slot,
                        new NumberField(sortOrder), new NumberField(textField.getSize()), textField, NumberField.WORD_0);
                final int actualCount = (int)response.getMenuResultsCount();
                if (actualCount == Message.NO_MENU_RESULTS_AVAILABLE || actualCount == 0) {
                    if (count != null) {
                        count.set(0);
                    }
                    return Collections.emptyList();
                }

                // Gather the requested number of search menu items
                if (count == null) {
                    return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slot, CdjStatus.TrackType.REKORDBOX, response);
                } else {
                    final int desiredCount = Math.min(count.get(), actualCount);
                    count.set(actualCount);
                    if (desiredCount < 1) {
                        return Collections.emptyList();
                    }
                    return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slot, CdjStatus.TrackType.REKORDBOX,
                            0, desiredCount);
                }
            } finally {
                client.unlockForMenuOperations();
            }
        } else {
            throw new TimeoutException("Unable to lock player for menu operations.");
        }
    }

    /**
     * Ask the specified player for database records whose names contain {@code text}. If {@code count} is not
     * {@code null}, no more than that many results will be returned, and the value will be set to the total number
     * of results that were available. Otherwise all results will be returned.
     *
     * @param player the player number whose database is to be searched
     * @param slot the slot in which the database can be found
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details, although it does not seem to have an effect on searches.
     * @param text the search text used to filter the results
     * @param count if present, sets an upper limit on the number of results to return, and will get set
     *              to the actual number that were available
     *
     * @return the items that the specified search string; they may be a variety of different types
     *
     * @throws Exception if there is a problem performing the search
     */
    public List requestSearchResultsFrom(final int player, final CdjStatus.TrackSourceSlot slot,
                                                  final int sortOrder, final String text,
                                                  final AtomicInteger count)
            throws Exception {
        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                return getSearchItems(slot, sortOrder, text.toUpperCase(), count, client);
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(player, task, "performing search");
    }


    /**
     * Ask the connected dbserver about database records whose names contain {@code text}. If {@code count} is not
     * {@code null}, no more than that many results will be returned, and the value will be set to the total number
     * of results that were available. Otherwise all results will be returned.
     *
     * @param slot the slot in which the database can be found
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details, although it does not seem to have an effect on searches.
     * @param text the search text used to filter the results
     * @param offset the first result desired (the first available result has offset 0)
     * @param count the number of results to return (if more than the number available, fewer will simply be returned)
     *
     * @return the items that match the specified search string; they may be a variety of different types
     *
     * @throws IOException if there is a problem communicating
     * @throws InterruptedException if the thread is interrupted while trying to lock the client for menu operations
     * @throws TimeoutException if we are unable to lock the client for menu operations
     */
    private List getMoreSearchItems(final CdjStatus.TrackSourceSlot slot, final int sortOrder, final String text,
                                             final int offset, final int count, final Client client)
            throws IOException, InterruptedException, TimeoutException {
        if (client.tryLockingForMenuOperations(MetadataFinder.MENU_TIMEOUT, TimeUnit.SECONDS)) {
            try {
                final StringField textField = new StringField(text);
                Message response = client.menuRequest(Message.KnownType.SEARCH_MENU, Message.MenuIdentifier.MAIN_MENU, slot,
                        new NumberField(sortOrder), new NumberField(textField.getSize()), textField, NumberField.WORD_0);
                final int actualCount = (int)response.getMenuResultsCount();
                if (offset + count > actualCount) {
                    throw new IllegalArgumentException("Cannot request items past the end of the menu.");
                }

                // Gather the requested search menu items
                return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slot, CdjStatus.TrackType.REKORDBOX,
                        offset, count);
            } finally {
                client.unlockForMenuOperations();
            }
        } else {
            throw new TimeoutException("Unable to lock player for menu operations.");
        }
    }

    /**
     * Ask the specified player for more database records whose names contain {@code text}. This can be used after
     * calling {@link #requestSearchResultsFrom(int, CdjStatus.TrackSourceSlot, int, String, AtomicInteger)} to obtain
     * a partial result and the total count available, to gradually expand the search under direction from the user.
     *
     * @param player the player number whose database is to be searched
     * @param slot the slot in which the database can be found
     * @param sortOrder the order in which responses should be sorted, 0 for default, see Section 6.11.1 of the
     *                  Packet Analysis
     *                  document for details, although it does not seem to have an effect on searches.
     * @param text the search text used to filter the results
     * @param offset the first result desired (the first available result has offset 0)
     * @param count the number of results to return (if more than the number available, fewer will simply be returned)
     *
     * @return the items that the specified search string; they may be a variety of different types
     *
     * @throws Exception if there is a problem performing the search
     */
    public List requestMoreSearchResultsFrom(final int player, final CdjStatus.TrackSourceSlot slot,
                                                      final int sortOrder, final String text,
                                                      final int offset, final int count)
            throws Exception {
        ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
            @Override
            public List useClient(Client client) throws Exception {
                return getMoreSearchItems(slot, sortOrder, text.toUpperCase(), offset, count, client);
            }
        };

        return ConnectionManager.getInstance().invokeWithClientSession(player, task, "performing search");
    }


    /**
     * Holds the singleton instance of this class.
     */
    private static final MenuLoader ourInstance = new MenuLoader();

    /**
     * Get the singleton instance of this class.
     *
     * @return the only instance of this class which exists.
     */
    public static MenuLoader getInstance() {
        return ourInstance;
    }

    /**
     * Prevent direct instantiation.
     */
    private MenuLoader() {
        // Nothing to do.
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy