Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
Watches for new tracks to be loaded on players, and queries the
* appropriate player for the metadata information when that happens.
*
*
Maintains a hot cache of metadata about any track currently loaded in a player, either on the main playback
* deck, or as a hot cue, since those tracks could start playing instantly.
*
*
Can also create cache files containing metadata about either all tracks in a media library, or tracks from a
* specific play list, and attach those cache files to be used instead of actually querying the player about tracks
* loaded from that library. This can be used in busy performance situations where all four usable player numbers
* are in use by actual players, to avoid conflicting queries yet still have useful metadata available. In such
* situations, you may want to go into passive mode, using {@link #setPassive(boolean)}, to prevent metadata queries
* about tracks that are not available from the attached metadata cache files.
*
* @author James Elliott
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class MetadataFinder extends LifecycleParticipant {
private static final Logger logger = LoggerFactory.getLogger(MetadataFinder.class);
/**
* Given a status update from a CDJ, find the metadata for the track that it has loaded, if any. If there is
* an appropriate metadata cache, will use that, otherwise makes a query to the players dbserver.
*
* @param status the CDJ status update that will be used to determine the loaded track and ask the appropriate
* player for metadata about it
*
* @return the metadata that was obtained, if any
*/
@SuppressWarnings("WeakerAccess")
public TrackMetadata requestMetadataFrom(final CdjStatus status) {
if (status.getTrackSourceSlot() == CdjStatus.TrackSourceSlot.NO_TRACK || status.getRekordboxId() == 0) {
return null;
}
final DataReference track = new DataReference(status.getTrackSourcePlayer(), status.getTrackSourceSlot(),
status.getRekordboxId());
return requestMetadataFrom(track, status.getTrackType());
}
/**
* Ask the specified player for metadata about the track in the specified slot with the specified rekordbox ID,
* unless we have a metadata cache available for the specified media slot, in which case that will be used instead.
*
* @param track uniquely identifies the track whose metadata is desired
* @param trackType identifies the type of track being requested, which affects the type of metadata request
* message that must be used
*
* @return the metadata, if any
*/
@SuppressWarnings("WeakerAccess")
public TrackMetadata requestMetadataFrom(final DataReference track, final CdjStatus.TrackType trackType) {
return requestMetadataInternal(track, trackType, false);
}
/**
* Ask the specified player for metadata about the track in the specified slot with the specified rekordbox ID,
* using cached media instead if it is available, and possibly giving up if we are in passive mode.
*
* @param track uniquely identifies the track whose metadata is desired
* @param trackType identifies the type of track being requested, which affects the type of metadata request
* message that must be used
* @param failIfPassive will prevent the request from taking place if we are in passive mode, so that automatic
* metadata updates will use available caches only
*
* @return the metadata found, if any
*/
private TrackMetadata requestMetadataInternal(final DataReference track, final CdjStatus.TrackType trackType,
final boolean failIfPassive) {
// First check if we are using cached data for this request
ZipFile cache = getMetadataCache(SlotReference.getSlotReference(track));
if (cache != null && trackType == CdjStatus.TrackType.REKORDBOX) {
return getCachedMetadata(cache, track);
}
if (passive.get() && failIfPassive) {
return null;
}
ConnectionManager.ClientTask task = new ConnectionManager.ClientTask() {
@Override
public TrackMetadata useClient(Client client) throws Exception {
return queryMetadata(track, trackType, client);
}
};
try {
return ConnectionManager.getInstance().invokeWithClientSession(track.player, task, "requesting metadata");
} catch (Exception e) {
logger.error("Problem requesting metadata, returning null", e);
}
return null;
}
/**
* How many seconds are we willing to wait to lock the database client for menu operations.
*/
public static final int MENU_TIMEOUT = 20;
/**
* Request metadata for a specific track ID, given a connection to a player that has already been set up.
* Separated into its own method so it could be used multiple times with the same connection when gathering
* all track metadata.
*
* @param track uniquely identifies the track whose metadata is desired
* @param trackType identifies the type of track being requested, which affects the type of metadata request
* message that must be used
* @param client the dbserver client that is communicating with the appropriate player
*
* @return the retrieved metadata, or {@code null} if there is no such track
*
* @throws IOException if there is a communication problem
* @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 TrackMetadata queryMetadata(final DataReference track, final CdjStatus.TrackType trackType, final Client client)
throws IOException, InterruptedException, TimeoutException {
// Send the metadata menu request
if (client.tryLockingForMenuOperations(20, TimeUnit.SECONDS)) {
try {
final Message.KnownType requestType = (trackType == CdjStatus.TrackType.REKORDBOX) ?
Message.KnownType.REKORDBOX_METADATA_REQ : Message.KnownType.UNANALYZED_METADATA_REQ;
final Message response = client.menuRequestTyped(requestType, Message.MenuIdentifier.MAIN_MENU,
track.slot, trackType, new NumberField(track.rekordboxId));
final long count = response.getMenuResultsCount();
if (count == Message.NO_MENU_RESULTS_AVAILABLE) {
return null;
}
// Gather the cue list and all the metadata menu items
final List items = client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, track.slot, trackType, response);
final CueList cueList = getCueList(track.rekordboxId, track.slot, client);
return new TrackMetadata(track, trackType, items, cueList);
} finally {
client.unlockForMenuOperations();
}
} else {
throw new TimeoutException("Unable to lock the player for menu operations");
}
}
/**
* Requests the cue list for a specific track ID, given a connection to a player that has already been set up.
*
* @param rekordboxId the track of interest
* @param slot identifies the media slot we are querying
* @param client the dbserver client that is communicating with the appropriate player
*
* @return the retrieved cue list, or {@code null} if none was available
* @throws IOException if there is a communication problem
*/
private CueList getCueList(int rekordboxId, CdjStatus.TrackSourceSlot slot, Client client)
throws IOException {
Message response = client.simpleRequest(Message.KnownType.CUE_LIST_REQ, null,
client.buildRMST(Message.MenuIdentifier.DATA, slot), new NumberField(rekordboxId));
if (response.knownType == Message.KnownType.CUE_LIST) {
return new CueList(response);
}
logger.error("Unexpected response type when requesting cue list: {}", response);
return null;
}
/**
* Request the list of all tracks in the specified slot, given a connection to a player that has already been
* set up.
*
* @param slot identifies the media slot we are querying
* @param client the dbserver client that is communicating with the appropriate player
*
* @return the retrieved track list entry items
*
* @throws IOException if there is a communication problem
* @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
*/
List getFullTrackList(final CdjStatus.TrackSourceSlot slot, final Client client, final int sortOrder)
throws IOException, InterruptedException, TimeoutException {
// Send the metadata menu request
if (client.tryLockingForMenuOperations(MENU_TIMEOUT, TimeUnit.SECONDS)) {
try {
Message response = client.menuRequest(Message.KnownType.TRACK_MENU_REQ, Message.MenuIdentifier.MAIN_MENU, slot,
new NumberField(sortOrder));
final long count = response.getMenuResultsCount();
if (count == Message.NO_MENU_RESULTS_AVAILABLE || count == 0) {
return Collections.emptyList();
}
// Gather all the metadata menu items
return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slot, CdjStatus.TrackType.REKORDBOX, response);
}
finally {
client.unlockForMenuOperations();
}
} else {
throw new TimeoutException("Unable to lock the player for menu operations");
}
}
/**
* Look up track metadata from a cache.
*
* @param cache the appropriate metadata cache file
* @param track identifies the track whose metadata is desired
*
* @return the cached metadata, including cue list (if available), or {@code null}
*/
public TrackMetadata getCachedMetadata(ZipFile cache, DataReference track) {
ZipEntry entry = cache.getEntry(getMetadataEntryName(track.rekordboxId));
if (entry != null) {
DataInputStream is = null;
try {
is = new DataInputStream(cache.getInputStream(entry));
List items = new LinkedList();
Message current = Message.read(is);
while (current.messageType.getValue() == Message.KnownType.MENU_ITEM.protocolValue) {
items.add(current);
current = Message.read(is);
}
return new TrackMetadata(track, CdjStatus.TrackType.REKORDBOX, items, getCachedCueList(cache, track.rekordboxId));
} catch (IOException e) {
logger.error("Problem reading metadata from cache file, returning null", e);
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
logger.error("Problem closing ZipFile input stream for reading metadata entry", e);
}
}
}
}
return null;
}
/**
* Look up a cue list in a metadata cache.
*
* @param cache the appropriate metadata cache file
* @param rekordboxId the track whose cue list is desired
*
* @return the cached cue list (if available), or {@code null}
*/
public CueList getCachedCueList(ZipFile cache, int rekordboxId) {
ZipEntry entry = cache.getEntry(getCueListEntryName(rekordboxId));
if (entry != null) {
DataInputStream is = null;
try {
is = new DataInputStream(cache.getInputStream(entry));
Message message = Message.read(is);
return new CueList(message);
} catch (IOException e) {
logger.error("Problem reading cue list from cache file, returning null", e);
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
logger.error("Problem closing ZipFile input stream for reading cue list", e);
}
}
}
}
return null;
}
/**
* Ask the connected dbserver for the playlist entries of the specified playlist (if {@code folder} is {@code false},
* or the list of playlists and folders inside the specified playlist folder (if {@code folder} is {@code true}.
* Pulled into a separate method so it can be used from multiple different client transactions.
*
* @param slot the slot in which the playlist 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
* @param playlistOrFolderId the database ID of the desired playlist or folder
* @param folder indicates whether we are asking for the contents of a folder or playlist
* @param client the dbserver client that is communicating with the appropriate player
* @return the items that are found in the specified playlist or folder; they will be tracks if we are asking
* for a playlist, or playlists and folders if we are asking for a folder
* @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 getPlaylistItems(CdjStatus.TrackSourceSlot slot, int sortOrder, int playlistOrFolderId,
boolean folder, Client client)
throws IOException, InterruptedException, TimeoutException {
if (client.tryLockingForMenuOperations(MENU_TIMEOUT, TimeUnit.SECONDS)) {
try {
Message response = client.menuRequest(Message.KnownType.PLAYLIST_REQ, Message.MenuIdentifier.MAIN_MENU, slot,
new NumberField(sortOrder), new NumberField(playlistOrFolderId), new NumberField(folder? 1 : 0));
final long count = response.getMenuResultsCount();
if (count == Message.NO_MENU_RESULTS_AVAILABLE || count == 0) {
return Collections.emptyList();
}
// Gather all the metadata menu items
return client.renderMenuItems(Message.MenuIdentifier.MAIN_MENU, slot, CdjStatus.TrackType.REKORDBOX, response);
} finally {
client.unlockForMenuOperations();
}
} else {
throw new TimeoutException("Unable to lock player for menu operations.");
}
}
/**
* Ask the specified player for the playlist entries of the specified playlist (if {@code folder} is {@code false},
* or the list of playlists and folders inside the specified playlist folder (if {@code folder} is {@code true}.
*
* @param player the player number whose playlist entries are of interest
* @param slot the slot in which the playlist 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
* @param playlistOrFolderId the database ID of the desired playlist or folder
* @param folder indicates whether we are asking for the contents of a folder or playlist
*
* @return the items that are found in the specified playlist or folder; they will be tracks if we are asking
* for a playlist, or playlists and folders if we are asking for a folder
*
* @throws Exception if there is a problem obtaining the playlist information
*/
public List requestPlaylistItemsFrom(final int player, final CdjStatus.TrackSourceSlot slot,
final int sortOrder, final int playlistOrFolderId,
final boolean folder)
throws Exception {
ConnectionManager.ClientTask> task = new ConnectionManager.ClientTask>() {
@Override
public List useClient(Client client) throws Exception {
return getPlaylistItems(slot, sortOrder, playlistOrFolderId, folder, client);
}
};
return ConnectionManager.getInstance().invokeWithClientSession(player, task, "requesting playlist information");
}
/**
* Creates a metadata cache archive file of all tracks in the specified slot on the specified player. Any
* previous contents of the specified file will be replaced.
*
* @param slot the slot in which the media to be cached can be found
* @param playlistId the id of playlist to be cached, or 0 of all tracks should be cached
* @param cache the file into which the metadata cache should be written
*
* @throws Exception if there is a problem communicating with the player or writing the cache file.
*/
public void createMetadataCache(SlotReference slot, int playlistId, File cache) throws Exception {
createMetadataCache(slot, playlistId, cache, null);
}
/**
* The root under which all zip file entries will be created in our cache metadata files.
*/
private static final String CACHE_PREFIX = "BLTMetaCache/";
/**
* The file entry whose content will be the cache format identifier.
*/
private static final String CACHE_FORMAT_ENTRY = CACHE_PREFIX + "version";
/**
* The prefix for cache file entries that will store track metadata.
*/
private static final String CACHE_METADATA_ENTRY_PREFIX = CACHE_PREFIX + "metadata/";
/**
* The prefix for cache file entries that will store album art.
*/
private static final String CACHE_ART_ENTRY_PREFIX = CACHE_PREFIX + "artwork/";
/**
* The prefix for cache file entries that will store beat grids.
*/
private static final String CACHE_BEAT_GRID_ENTRY_PREFIX = CACHE_PREFIX + "beatGrid/";
/**
* The prefix for cache file entries that will store beat grids.
*/
private static final String CACHE_CUE_LIST_ENTRY_PREFIX = CACHE_PREFIX + "cueList/";
/**
* The prefix for cache file entries that will store waveform previews.
*/
private static final String CACHE_WAVEFORM_PREVIEW_ENTRY_PREFIX = CACHE_PREFIX + "wavePrev/";
/**
* The prefix for cache file entries that will store waveform previews.
*/
private static final String CACHE_WAVEFORM_DETAIL_ENTRY_PREFIX = CACHE_PREFIX + "waveform/";
/**
* The comment string used to identify a ZIP file as one of our metadata caches.
*/
@SuppressWarnings("WeakerAccess")
public static final String CACHE_FORMAT_IDENTIFIER = "BeatLink Metadata Cache version 1";
/**
* Used to mark the end of the metadata items in each cache entry, just like when reading from the server.
*/
private static final Message MENU_FOOTER_MESSAGE = new Message(0, Message.KnownType.MENU_FOOTER);
/**
* How long should we pause between requesting metadata entries while building a cache to give the player
* a chance to perform its other tasks.
*/
private final AtomicLong cachePauseInterval = new AtomicLong(50);
/**
* Set how long to pause between requesting metadata entries while building a cache to give the player
* a chance to perform its other tasks.
*
* @param milliseconds the delay to add between each track that gets added to the metadata cache
*/
public void setCachePauseInterval(long milliseconds) {
cachePauseInterval.set(milliseconds);
}
/**
* Check how long we pause between requesting metadata entries while building a cache to give the player
* a chance to perform its other tasks.
*
* @return the delay to add between each track that gets added to the metadata cache
*/
public long getCachePauseInterval() {
return cachePauseInterval.get();
}
/**
* Finish the process of copying a list of tracks to a metadata cache, once they have been listed. This code
* is shared between the implementations that work with the full track list and with playlists.
*
* @param trackListEntries the list of menu items identifying which tracks need to be copied to the metadata
* cache
* @param playlistId the id of playlist being cached, or 0 of all tracks are being cached
* @param client the connection to the dbserver on the player whose metadata is being cached
* @param slot the slot in which the media to be cached can be found
* @param cache the file into which the metadata cache should be written
* @param listener will be informed after each track is added to the cache file being created and offered
* the opportunity to cancel the process
*
* @throws IOException if there is a problem communicating with the player or writing the cache file.
* @throws TimeoutException if we are unable to lock the client for menu operations
*/
private void copyTracksToCache(List trackListEntries, int playlistId, Client client, SlotReference slot,
File cache, MetadataCacheCreationListener listener)
throws IOException, TimeoutException {
FileOutputStream fos = null;
BufferedOutputStream bos = null;
ZipOutputStream zos = null;
WritableByteChannel channel = null;
final Set tracksAdded = new HashSet();
final Set artworkAdded = new HashSet();
try {
fos = new FileOutputStream(cache);
bos = new BufferedOutputStream(fos);
zos = new ZipOutputStream(bos);
zos.setMethod(ZipOutputStream.DEFLATED);
// Add a marker so we can recognize this as a metadata archive. I would use the ZipFile comment, but
// that is not available until Java 7, and Beat Link is supposed to be backwards compatible with Java 6.
// Since we are doing this anyway, we can also provide information about the nature of the cache, and
// how many metadata entries it contains, which is useful for auto-attachment.
zos.putNextEntry(new ZipEntry(CACHE_FORMAT_ENTRY));
String formatEntry = CACHE_FORMAT_IDENTIFIER + ":" + playlistId + ":" + trackListEntries.size();
zos.write(formatEntry.getBytes("UTF-8"));
// Write the actual metadata entries
channel = Channels.newChannel(zos);
final int totalToCopy = trackListEntries.size();
TrackMetadata lastTrackAdded = null;
int tracksCopied = 0;
for (Message entry : trackListEntries) {
if (entry.getMenuItemType() == Message.MenuItemType.UNKNOWN) {
logger.warn("Encountered unrecognized track list entry item type: {}", entry);
}
int rekordboxId = (int)((NumberField)entry.arguments.get(1)).getValue();
if (!tracksAdded.contains(rekordboxId)) { // Ignore extra copies of a track present on a playlist.
lastTrackAdded = copyTrackToCache(client, slot, zos, channel, artworkAdded, rekordboxId);
tracksAdded.add(rekordboxId);
}
if (listener != null) {
if (!listener.cacheCreationContinuing(lastTrackAdded, ++tracksCopied, totalToCopy)) {
logger.info("Track metadata cache creation canceled by listener");
if (!cache.delete()) {
logger.warn("Unable to delete metadata cache file, {}", cache);
}
return;
}
}
Thread.sleep(cachePauseInterval.get());
}
} catch (InterruptedException e) {
logger.warn("Interrupted while building metadata cache file, aborting", e);
if (!cache.delete()) {
logger.warn("Unable to delete metadata cache file, {}", cache);
}
} finally {
try {
if (channel != null) {
channel.close();
}
} catch (Exception e) {
logger.error("Problem closing byte channel for writing to metadata cache", e);
}
try {
if (zos != null) {
zos.close();
}
} catch (Exception e) {
logger.error("Problem closing Zip Output Stream of metadata cache", e);
}
try {
if (bos != null) {
bos.close();
}
} catch (Exception e) {
logger.error("Problem closing Buffered Output Stream of metadata cache", e);
}
try {
if (fos != null) {
fos.close();
}
} catch (Exception e) {
logger.error("Problem closing File Output Stream of metadata cache", e);
}
}
}
/**
* Copy a single track's metadata and related objects to a cache file being created.
*
* @param client the connection to the database server from which the art can be obtained
* @param slot the player slot from which the art is being copied
* @param zos the stream to which the cache is being written
* @param channel the low-level channel to which the cache is being written
* @param artworkAdded collects the artwork that has already been added to the cache, to avoid duplicates
* @param rekordboxId the database ID of the track to be cached
*
* @return the track metadata object that was written to the cache, or {@code null} if it could not be found
*
* @throws IOException if there is a problem communicating with the player or writing to the cache file
* @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 TrackMetadata copyTrackToCache(Client client, SlotReference slot, ZipOutputStream zos,
WritableByteChannel channel, Set artworkAdded, int rekordboxId)
throws IOException, TimeoutException, InterruptedException {
final TrackMetadata track = queryMetadata(new DataReference(slot, rekordboxId), CdjStatus.TrackType.REKORDBOX, client);
if (track != null) {
logger.debug("Adding metadata with ID {}", track.trackReference.rekordboxId);
zos.putNextEntry(new ZipEntry(getMetadataEntryName(track.trackReference.rekordboxId)));
for (Message metadataItem : track.rawItems) {
metadataItem.write(channel);
}
MENU_FOOTER_MESSAGE.write(channel); // So we know to stop reading
} else {
logger.warn("Unable to retrieve metadata with ID {}", rekordboxId);
return null;
}
if (track.getArtworkId() != 0 && !artworkAdded.contains(track.getArtworkId())) {
logger.debug("Adding artwork with ID {}", track.getArtworkId());
zos.putNextEntry(new ZipEntry(getArtworkEntryName(track.getArtworkId())));
final AlbumArt art = ArtFinder.getInstance().getArtwork(track.getArtworkId(), slot, CdjStatus.TrackType.REKORDBOX, client);
if (art != null) {
Util.writeFully(art.getRawBytes(), channel);
artworkAdded.add(track.getArtworkId());
}
}
final BeatGrid beatGrid = BeatGridFinder.getInstance().getBeatGrid(rekordboxId, slot, client);
if (beatGrid != null) {
logger.debug("Adding beat grid with ID {}", rekordboxId);
zos.putNextEntry(new ZipEntry(getBeatGridEntryName(rekordboxId)));
Util.writeFully(beatGrid.getRawData(), channel);
}
final CueList cueList = getCueList(rekordboxId, slot.slot, client);
if (cueList != null) {
logger.debug("Adding cue list entry with ID {}", rekordboxId);
zos.putNextEntry(new ZipEntry((getCueListEntryName(rekordboxId))));
cueList.rawMessage.write(channel);
}
final WaveformPreview preview = WaveformFinder.getInstance().getWaveformPreview(rekordboxId, slot, client);
if (preview != null) {
logger.debug("Adding waveform preview entry with ID {}", rekordboxId);
zos.putNextEntry(new ZipEntry((getWaveformPreviewEntryName(rekordboxId))));
preview.rawMessage.write(channel);
}
final WaveformDetail detail = WaveformFinder.getInstance().getWaveformDetail(rekordboxId, slot, client);
if (detail != null) {
logger.debug("Adding waveform detail entry with ID {}", rekordboxId);
zos.putNextEntry(new ZipEntry((getWaveformDetailEntryName(rekordboxId))));
detail.rawMessage.write(channel);
}
return track;
}
/**
* Names the appropriate zip file entry for caching a track's metadata.
*
* @param rekordboxId the id of the track being cached or looked up
*
* @return the name of the entry where that track's metadata should be stored
*/
private String getMetadataEntryName(int rekordboxId) {
return CACHE_METADATA_ENTRY_PREFIX + rekordboxId;
}
/**
* Names the appropriate zip file entry for caching album art.
*
* @param artworkId the database ID of the artwork being cached or looked up
*
* @return the name of entry where that artwork should be stored
*/
String getArtworkEntryName(int artworkId) {
return CACHE_ART_ENTRY_PREFIX + artworkId + ".jpg";
}
/**
* Names the appropriate zip file entry for caching a track's beat grid.
*
* @param rekordboxId the id of the track being cached or looked up
*
* @return the name of the entry where that track's beat grid should be stored
*/
String getBeatGridEntryName(int rekordboxId) {
return CACHE_BEAT_GRID_ENTRY_PREFIX + rekordboxId;
}
/**
* Names the appropriate zip file entry for caching a track's cue list.
*
* @param rekordboxId the id of the track being cached or looked up
*
* @return the name of the entry where that track's cue list should be stored
*/
private String getCueListEntryName(int rekordboxId) {
return CACHE_CUE_LIST_ENTRY_PREFIX + rekordboxId;
}
/**
* Names the appropriate zip file entry for caching a track's waveform preview.
*
* @param rekordboxId the id of the track being cached or looked up
*
* @return the name of the entry where that track's waveform preview should be stored
*/
String getWaveformPreviewEntryName(int rekordboxId) {
return CACHE_WAVEFORM_PREVIEW_ENTRY_PREFIX + rekordboxId;
}
/**
* Names the appropriate zip file entry for caching a track's waveform detail.
*
* @param rekordboxId the id of the track being cached or looked up
*
* @return the name of the entry where that track's waveform detail should be stored
*/
String getWaveformDetailEntryName(int rekordboxId) {
return CACHE_WAVEFORM_DETAIL_ENTRY_PREFIX + rekordboxId;
}
/**
* Creates a metadata cache archive file of all tracks in the specified slot on the specified player. Any
* previous contents of the specified file will be replaced. If a non-{@code null} {@code listener} is
* supplied, its {@link MetadataCacheCreationListener#cacheCreationContinuing(TrackMetadata, int, int)} method
* will be called after each track is added to the cache, allowing it to display progress updates to the user,
* and to continue or cancel the process by returning {@code true} or {@code false}.
*
* Because this takes a huge amount of time relative to CDJ status updates, it can only be performed while
* the MetadataFinder is in passive mode.
*
* @param slot the slot in which the media to be cached can be found
* @param playlistId the id of playlist to be cached, or 0 of all tracks should be cached
* @param cache the file into which the metadata cache should be written
* @param listener will be informed after each track is added to the cache file being created and offered
* the opportunity to cancel the process
*
* @throws Exception if there is a problem communicating with the player or writing the cache file
*/
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public void createMetadataCache(final SlotReference slot, final int playlistId,
final File cache, final MetadataCacheCreationListener listener)
throws Exception {
ConnectionManager.ClientTask