Skip to content

Commit df9aa4e

Browse files
committed
Change archive creation strategy
No longer rely on DeviceSQL track records, because we may be using the SQLite database instead, which references different artwork and possibly different analysis files as well.
1 parent b745e72 commit df9aa4e

File tree

1 file changed

+120
-68
lines changed

1 file changed

+120
-68
lines changed
Lines changed: 120 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package org.deepsymmetry.cratedigger;
22

33
import org.apiguardian.api.API;
4-
import org.deepsymmetry.cratedigger.pdb.RekordboxPdb;
5-
import org.slf4j.Logger;
6-
import org.slf4j.LoggerFactory;
74

85
import java.io.File;
96
import java.io.IOException;
107
import java.net.URI;
118
import java.net.URISyntaxException;
129
import java.nio.file.*;
13-
import java.util.Iterator;
10+
import java.nio.file.attribute.BasicFileAttributes;
1411
import java.util.Map;
12+
import java.util.concurrent.atomic.AtomicBoolean;
13+
import java.util.concurrent.atomic.AtomicLong;
1514

1615
/**
1716
* Supports the creation of archives of all the metadata needed from rekordbox media exports to enable full Beat Link
@@ -20,8 +19,6 @@
2019
@API(status = API.Status.EXPERIMENTAL)
2120
public class Archivist {
2221

23-
private static final Logger logger = LoggerFactory.getLogger(Archivist.class);
24-
2522
/**
2623
* Holds the singleton instance of this class.
2724
*/
@@ -52,15 +49,27 @@ private Archivist() {
5249
public interface ArchiveListener {
5350

5451
/**
55-
* Called once we determine how many tracks need to be archived, and as each one is completed, so that
56-
* progress can be displayed; the process can be canceled by returning {@code false}.
52+
* Called as each analysis or artwork file is archived, so that progress can be displayed;
53+
* the process can be canceled by returning {@code false}.
5754
*
58-
* @param tracksCompleted how many tracks have been added to the archive
59-
* @param tracksTotal how many tracks are present in the media export being archived
55+
* @param bytesCopied how many bytes of analysis and artwork files have been added to the archive
56+
* @param bytesTotal how many bytes of analysis and artwork files are present in the media export being archived
6057
*
6158
* @return {@code true} to continue archiving tracks, or {@code false} to cancel the process and delete the archive.
6259
*/
63-
boolean continueCreating(int tracksCompleted, int tracksTotal);
60+
boolean continueCreating(long bytesCopied, long bytesTotal);
61+
}
62+
63+
/**
64+
* Allows our recursive file copy operations to exclude files that we do not want in the archive.
65+
*/
66+
private interface PathFilter {
67+
/**
68+
* Check whether something belongs in the archive
69+
* @param path the file that will potentially be copied
70+
* @return {@code true} to actually copy the file.
71+
*/
72+
boolean include(Path path);
6473
}
6574

6675
/**
@@ -78,6 +87,87 @@ public void createArchive(Database database, File file) throws IOException {
7887
createArchive(database, file, null);
7988
}
8089

90+
/**
91+
* Helper method to recursively count the number of file bytes that will be copied if we copy a folder.
92+
*
93+
* @param source the folder to be copied
94+
* @param filter if present, allows files to be selectively excluded from being counted
95+
*
96+
* @return the new total number of bytes that need to be copied.
97+
*
98+
* @throws IOException if there is a problem scanning the folder
99+
*/
100+
private long sizeFolder(Path source, PathFilter filter)
101+
throws IOException {
102+
103+
final AtomicLong totalBytes = new AtomicLong(0);
104+
105+
Files.walkFileTree(source, new SimpleFileVisitor<>() {
106+
107+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
108+
throws IOException {
109+
if (filter == null || filter.include(file)) {
110+
totalBytes.addAndGet(Files.size(file));
111+
}
112+
return FileVisitResult.CONTINUE;
113+
}
114+
});
115+
116+
return totalBytes.get();
117+
}
118+
119+
/**
120+
* Helper method to recursively copy a folder.
121+
*
122+
* @param source the folder to be copied
123+
* @param target where the folder should be copied
124+
* @param filter if present, allows files to be selectively excluded from being counted
125+
* @param listener if not {@code null} will be called after copying each file to support progress reports and
126+
* allow cancellation
127+
* @param bytesCopied the number of bytes that have already been copied, for use in updating the listener
128+
* @param totalBytes the total number of bytes that are going to be copied, for use in updating the listener
129+
* @param options the copy options (see {@link Files#copy(Path, Path, CopyOption...)})
130+
*
131+
* @return the new total number of bytes copied, or -1 if the listener requested that the copy be canceled.
132+
*
133+
* @throws IOException if there is a problem copying the folder
134+
*/
135+
private long copyFolder(Path source, Path target, PathFilter filter, ArchiveListener listener, long bytesCopied, long totalBytes, CopyOption... options)
136+
throws IOException {
137+
138+
final AtomicLong nowCopied = new AtomicLong(bytesCopied);
139+
final AtomicBoolean canceled = new AtomicBoolean(false);
140+
141+
Files.walkFileTree(source, new SimpleFileVisitor<>() {
142+
143+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
144+
throws IOException {
145+
Files.createDirectories(target.resolve(source.relativize(dir).toString()));
146+
return FileVisitResult.CONTINUE;
147+
}
148+
149+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
150+
throws IOException {
151+
if (filter == null || filter.include(file)) {
152+
Files.copy(file, target.resolve(source.relativize(file).toString()), options);
153+
nowCopied.addAndGet(Files.size(file));
154+
if (listener == null || listener.continueCreating(nowCopied.get(), totalBytes)) {
155+
return FileVisitResult.CONTINUE;
156+
} else {
157+
canceled.set(true);
158+
return FileVisitResult.TERMINATE;
159+
}
160+
}
161+
return FileVisitResult.CONTINUE;
162+
}
163+
});
164+
165+
if (canceled.get()) {
166+
return -1;
167+
}
168+
return nowCopied.get();
169+
}
170+
81171
/**
82172
* Creates an archive file containing all the metadata found in the rekordbox media export containing the
83173
* supplied database export that needed to enable full Beat Link features when that media is being used in
@@ -93,10 +183,10 @@ public void createArchive(Database database, File file) throws IOException {
93183
@API(status = API.Status.EXPERIMENTAL)
94184
public void createArchive(Database database, File archiveFile, ArchiveListener listener) throws IOException {
95185
final Path archivePath = archiveFile.toPath();
96-
final Path mediaPath = database.sourceFile.getParentFile().getParentFile().getParentFile().toPath();
97186
Files.deleteIfExists(archivePath);
98187
final URI fileUri = archivePath.toUri();
99-
final int totalTracks = database.trackIndex.size();
188+
// We want to exclude .2EX files since we can’t use them, and they bloat the archive.
189+
final PathFilter analysisFilter = path -> !path.toString().endsWith(".2EX");
100190
boolean failed = false;
101191

102192
try (FileSystem fileSystem = FileSystems.newFileSystem(new URI("jar:" + fileUri.getScheme(), fileUri.getPath(), null),
@@ -111,37 +201,24 @@ public void createArchive(Database database, File archiveFile, ArchiveListener l
111201
Files.copy(plusFile.toPath(), fileSystem.getPath("/exportLibrary.db"));
112202
}
113203

114-
// Copy each track's analysis and artwork files.
115-
final Iterator<Map.Entry<Long, RekordboxPdb.TrackRow>> iterator = database.trackIndex.entrySet().iterator();
116-
int completed = 0;
117-
while ((listener == null || listener.continueCreating(completed, totalTracks)) && iterator.hasNext()) {
118-
final Map.Entry<Long, RekordboxPdb.TrackRow> entry = iterator.next();
119-
final RekordboxPdb.TrackRow track = entry.getValue();
120-
121-
// First the original analysis file.
122-
final String anlzPathString = Database.getText(track.analyzePath());
123-
archiveMediaItem(mediaPath, anlzPathString, fileSystem, "analysis file", true);
124-
125-
// Then the extended analysis file, if it exists.
126-
final String extPathString = anlzPathString.substring(0, anlzPathString.length() - 3) + "EXT";
127-
archiveMediaItem(mediaPath, extPathString, fileSystem, "extended analysis file", false);
128-
129-
// Finally, the album art.
130-
final RekordboxPdb.ArtworkRow artwork = database.artworkIndex.get(track.artworkId());
131-
if (artwork != null) {
132-
final String artPathString = Database.getText(artwork.path());
133-
archiveMediaItem(mediaPath, artPathString, fileSystem, "artwork file", true);
134-
135-
// Then, copy the high resolution album art, if it exists
136-
final String highResArtPathString = artPathString.replaceFirst("(\\.\\w+$)", "_m$1");
137-
archiveMediaItem(mediaPath, highResArtPathString, fileSystem, "high-resolution artwork file", false);
204+
// Copy the track analysis and artwork files.
205+
final Path pioneerFolder = plusFile.toPath().getParent().getParent();
206+
final Path artFolder = pioneerFolder.resolve("Artwork");
207+
//noinspection SpellCheckingInspection
208+
final Path analysisFolder = pioneerFolder.resolve("USBANLZ");
209+
final long totalBytes = sizeFolder(artFolder, null) + sizeFolder(analysisFolder, analysisFilter);
210+
long bytesCopied = copyFolder(artFolder, fileSystem.getPath("PIONEER", "Artwork"), null, listener,
211+
0, totalBytes, StandardCopyOption.REPLACE_EXISTING);
212+
if (bytesCopied < 0) {
213+
// Listener asked us to cancel.
214+
failed = true;
215+
} else {
216+
//noinspection SpellCheckingInspection
217+
bytesCopied = copyFolder(analysisFolder, fileSystem.getPath("PIONEER", "USBANLZ"), analysisFilter, listener,
218+
bytesCopied, totalBytes, StandardCopyOption.REPLACE_EXISTING);
219+
if (bytesCopied < 0) {
220+
failed = true;
138221
}
139-
140-
++completed; // For use in providing progress feedback if there is a listener.
141-
}
142-
143-
if (iterator.hasNext()) {
144-
failed = true; // We were canceled.
145222
}
146223
} catch (URISyntaxException e) {
147224
failed = true;
@@ -157,29 +234,4 @@ public void createArchive(Database database, File archiveFile, ArchiveListener l
157234
}
158235
}
159236

160-
/**
161-
* Helper method to archive a single media export file when creating a metadata archive.
162-
*
163-
* @param mediaPath the path to the file to be archived
164-
* @param pathString the string which holds the absolute path to the media item
165-
* @param archive the ZIP filesystem in which the metadata archive is being created
166-
* @param description the text identifying the type of file being archived, in case we need to log a warning
167-
* @param expected indicates whether a file is always supposed to be there, so we should warn about its absence
168-
*
169-
* @throws IOException if there is an unexpected problem adding the media item to the archive
170-
*/
171-
private static void archiveMediaItem(Path mediaPath, String pathString, FileSystem archive, String description, boolean expected) throws IOException {
172-
final Path sourcePath = mediaPath.resolve(pathString.substring(1));
173-
if (sourcePath.toFile().canRead()) {
174-
final Path destinationPath = archive.getPath(pathString);
175-
Files.createDirectories(destinationPath.getParent());
176-
try {
177-
Files.copy(sourcePath, destinationPath);
178-
} catch (FileAlreadyExistsException e) {
179-
logger.warn("Skipping copy of {} {} because it has already been archived." , description, destinationPath);
180-
}
181-
} else if (expected) {
182-
logger.warn("Could not find expected {} {}, omitting from archive.", description, sourcePath);
183-
}
184-
}
185237
}

0 commit comments

Comments
 (0)