11package org .deepsymmetry .cratedigger ;
22
33import org .apiguardian .api .API ;
4- import org .deepsymmetry .cratedigger .pdb .RekordboxPdb ;
5- import org .slf4j .Logger ;
6- import org .slf4j .LoggerFactory ;
74
85import java .io .File ;
96import java .io .IOException ;
107import java .net .URI ;
118import java .net .URISyntaxException ;
129import java .nio .file .*;
13- import java .util . Iterator ;
10+ import java .nio . file . attribute . BasicFileAttributes ;
1411import 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
2019@ API (status = API .Status .EXPERIMENTAL )
2120public 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