diff --git a/CHANGELOG.md b/CHANGELOG.md index 1272e84..e48283a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ This change log follows the conventions of This means that `CueTag.lenCues` and `ExtendedCueTag.lenCues` are now `CueTag.numCues` and `ExtendedCueTag.numCues` in the generated Java classes. +- Also upgraded to Java 11 as a compilation target, in order to be able to work with the new IO API, among other things. + +### Added + +- New API to create an archive of all the metadata from a media export volume needed to support Beat Link (to support working with the Opus Quad, which cannot provide metadata itself). ### Fixed diff --git a/pom.xml b/pom.xml index 58287e6..d19132c 100644 --- a/pom.xml +++ b/pom.xml @@ -288,7 +288,7 @@ 3.2.2 - 9 + 11 @@ -300,7 +300,9 @@ org.apache.maven.plugins maven-compiler-plugin - 6 + 11 + 11 + 11 diff --git a/src/main/java/org/deepsymmetry/cratedigger/Archivist.java b/src/main/java/org/deepsymmetry/cratedigger/Archivist.java new file mode 100644 index 0000000..3cce9a1 --- /dev/null +++ b/src/main/java/org/deepsymmetry/cratedigger/Archivist.java @@ -0,0 +1,143 @@ +package org.deepsymmetry.cratedigger; + +import org.deepsymmetry.cratedigger.pdb.RekordboxPdb; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Map; + +/** + * Supports the creation of archives of all the metadata needed from rekordbox media exports to enable full Beat Link + * features when working with the Opus Quad, which is unable to serve the metadata itself. + */ +public class Archivist { + + /** + * Holds the singleton instance of this class. + */ + private static final Archivist instance = new Archivist(); + + /** + * Look up the singleton instance of this class. + * + * @return the only instance that exists + */ + public static Archivist getInstance() { + return instance; + } + + /** + * Make sure the only way to get an instance is to call {@link #getInstance()}. + */ + private Archivist() { + // Prevent instantiation + } + + /** + * An interface that can be used to display progress to the user as an archive is being created, and allow + * them to cancel the process if desired. + */ + public interface ArchiveListener { + + /** + * Called once we determine how many tracks need to be archived, and as each one is completed, so that + * progress can be displayed; the process can be canceled by returning {@code false}. + * + * @param tracksCompleted how many tracks have been added to the archive + * @param tracksTotal how many tracks are present in the media export being archived + * + * @return {@code true} to continue archiving tracks, or {@code false} to cancel the process and delete the archive. + */ + boolean continueCreating(int tracksCompleted, int tracksTotal); + } + + /** + * Creates an archive file containing all the metadata found in the rekordbox media export containing the + * supplied database export that needed to enable full Beat Link features when that media is being used in + * an Opus Quad, which is unable to serve the metadata itself. + * + * @param database the parsed database found within the media export for which an archive is desired + * @param file where to write the archive + * + * @throws IOException if there is a problem creating the archive + */ + public void createArchive(Database database, File file) throws IOException { + createArchive(database, file, null); + } + + /** + * Creates an archive file containing all the metadata found in the rekordbox media export containing the + * supplied database export that needed to enable full Beat Link features when that media is being used in + * an Opus Quad, which is unable to serve the metadata itself. + * + * @param database the parsed database found within the media export for which an archive is desired + * @param archiveFile where to write the archive, will be replaced if it already exists + * @param listener if not {@code null}, will be called throughout the archive process to support progress + * reports and allow cancellation + * + * @throws IOException if there is a problem creating the archive + */ + public void createArchive(Database database, File archiveFile, ArchiveListener listener) throws IOException { + final Path archivePath = archiveFile.toPath(); + final Path mediaPath = database.sourceFile.getParentFile().getParentFile().getParentFile().toPath(); + Files.deleteIfExists(archivePath); + final URI fileUri = archivePath.toUri(); + final int totalTracks = database.trackIndex.size(); + try (FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + fileUri.getScheme(), fileUri.getPath(), null), + Map.of("create", "true"))) { + + // Copy the database export itself. + Files.copy(database.sourceFile.toPath(), fileSystem.getPath("/export.pdb")); + + // Copy each track's analysis and artwork files. + final Iterator> iterator = database.trackIndex.entrySet().iterator(); + int completed = 0; + while ((listener == null || listener.continueCreating(completed, totalTracks)) && iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + final RekordboxPdb.TrackRow track = entry.getValue(); + + // First the original analysis file. + final String anlzPathString = Database.getText(track.analyzePath()); + final Path anlzPath = mediaPath.resolve(anlzPathString.substring(1)); + Path destPath = fileSystem.getPath(anlzPathString); + Files.createDirectories(destPath.getParent()); + Files.copy(anlzPath, destPath); + + // Then the extended analysis file, if it exists. + final String extPathString = anlzPathString.substring(0, anlzPathString.length() - 3) + "EXT"; + final Path extPath = mediaPath.resolve(extPathString.substring(1)); + if (extPath.toFile().canRead()) { + destPath = fileSystem.getPath(extPathString); + Files.copy(extPath, destPath); + } + + // Finally, the album art. + final RekordboxPdb.ArtworkRow artwork = database.artworkIndex.get(track.artworkId()); + if (artwork != null) { + final String artPathString = Database.getText(artwork.path()); + final Path artPath = mediaPath.resolve(artPathString.substring(1)); + if (artPath.toFile().canRead()) { + destPath = fileSystem.getPath(artPathString); + Files.createDirectories(destPath.getParent()); + Files.copy(artPath, destPath); + } + } + + ++completed; // For use in providing progress feedback if there is a listener. + } + } catch (URISyntaxException e) { + Files.deleteIfExists(archivePath); + throw new IOException("Unable to create jar filesystem at file location", e); + } catch (IOException e) { + Files.deleteIfExists(archivePath); + throw e; + } + } +}