2014-04-19 02:30:29 -04:00
|
|
|
package net.filebot.ui.rename;
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2016-04-13 14:41:06 -04:00
|
|
|
import static java.util.Arrays.*;
|
2009-05-17 16:58:20 -04:00
|
|
|
import static java.util.Collections.*;
|
2016-05-15 15:26:26 -04:00
|
|
|
import static java.util.stream.Collectors.*;
|
2016-03-09 11:02:36 -05:00
|
|
|
import static net.filebot.Logging.*;
|
2014-04-19 02:30:29 -04:00
|
|
|
import static net.filebot.Settings.*;
|
2016-04-13 14:41:06 -04:00
|
|
|
import static net.filebot.media.MediaDetection.*;
|
2016-03-27 09:52:59 -04:00
|
|
|
import static net.filebot.media.XattrMetaInfo.*;
|
2014-04-19 02:30:29 -04:00
|
|
|
import static net.filebot.util.ExceptionUtilities.*;
|
|
|
|
import static net.filebot.util.FileUtilities.*;
|
2014-07-29 02:45:15 -04:00
|
|
|
import static net.filebot.util.ui.SwingUI.*;
|
2009-05-17 16:58:20 -04:00
|
|
|
|
|
|
|
import java.awt.Window;
|
2007-12-23 14:28:04 -05:00
|
|
|
import java.awt.event.ActionEvent;
|
|
|
|
import java.io.File;
|
2009-05-17 16:58:20 -04:00
|
|
|
import java.util.AbstractList;
|
2009-05-02 19:34:04 -04:00
|
|
|
import java.util.ArrayList;
|
2011-11-04 03:45:48 -04:00
|
|
|
import java.util.LinkedHashMap;
|
2009-05-02 19:34:04 -04:00
|
|
|
import java.util.List;
|
2009-05-17 16:58:20 -04:00
|
|
|
import java.util.Map;
|
2012-02-10 11:43:09 -05:00
|
|
|
import java.util.Map.Entry;
|
2011-11-04 03:45:48 -04:00
|
|
|
import java.util.Set;
|
2016-04-14 05:32:03 -04:00
|
|
|
import java.util.TreeSet;
|
2012-07-17 16:52:03 -04:00
|
|
|
import java.util.concurrent.CancellationException;
|
2016-05-07 13:55:48 -04:00
|
|
|
import java.util.function.BiConsumer;
|
|
|
|
import java.util.function.Consumer;
|
|
|
|
import java.util.function.Supplier;
|
2011-11-04 03:45:48 -04:00
|
|
|
import java.util.logging.Level;
|
2014-08-08 15:15:37 -04:00
|
|
|
import java.util.stream.Stream;
|
2007-12-23 14:28:04 -05:00
|
|
|
|
|
|
|
import javax.swing.AbstractAction;
|
|
|
|
|
2014-04-19 02:30:29 -04:00
|
|
|
import net.filebot.HistorySpooler;
|
|
|
|
import net.filebot.NativeRenameAction;
|
|
|
|
import net.filebot.ResourceManager;
|
|
|
|
import net.filebot.StandardRenameAction;
|
2016-08-16 15:37:30 -04:00
|
|
|
import net.filebot.UserFiles;
|
2014-11-03 07:22:45 -05:00
|
|
|
import net.filebot.mac.MacAppUtilities;
|
2014-04-19 02:30:29 -04:00
|
|
|
import net.filebot.similarity.Match;
|
2016-05-07 13:55:48 -04:00
|
|
|
import net.filebot.util.ui.ProgressMonitor;
|
|
|
|
import net.filebot.util.ui.ProgressMonitor.ProgressWorker;
|
2007-12-23 14:28:04 -05:00
|
|
|
|
2009-01-17 06:03:09 -05:00
|
|
|
class RenameAction extends AbstractAction {
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2012-07-12 07:23:23 -04:00
|
|
|
public static final String RENAME_ACTION = "RENAME_ACTION";
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2009-04-26 09:34:22 -04:00
|
|
|
private final RenameModel model;
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2009-04-26 09:34:22 -04:00
|
|
|
public RenameAction(RenameModel model) {
|
2009-05-17 16:58:20 -04:00
|
|
|
this.model = model;
|
2012-07-12 07:23:23 -04:00
|
|
|
resetValues();
|
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2012-07-12 07:23:23 -04:00
|
|
|
public void resetValues() {
|
|
|
|
putValue(RENAME_ACTION, StandardRenameAction.MOVE);
|
2009-05-17 16:58:20 -04:00
|
|
|
putValue(NAME, "Rename");
|
|
|
|
putValue(SMALL_ICON, ResourceManager.getIcon("action.rename"));
|
2007-12-23 14:28:04 -05:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2012-07-19 23:38:47 -04:00
|
|
|
@Override
|
2009-01-11 16:23:03 -05:00
|
|
|
public void actionPerformed(ActionEvent evt) {
|
2016-04-26 04:09:34 -04:00
|
|
|
if (model.names().isEmpty() || model.files().isEmpty()) {
|
|
|
|
log.info("Nothing to rename. New Names is empty. Please <Fetch Data> first.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2009-01-11 16:23:03 -05:00
|
|
|
try {
|
2016-04-14 05:32:03 -04:00
|
|
|
Window window = getWindow(evt.getSource());
|
|
|
|
withWaitCursor(window, () -> {
|
2016-08-16 15:37:30 -04:00
|
|
|
Map<File, File> renameMap = validate(model.getRenameMap(), window);
|
|
|
|
|
2016-04-14 05:32:03 -04:00
|
|
|
if (renameMap.isEmpty()) {
|
|
|
|
return;
|
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-04-14 05:32:03 -04:00
|
|
|
List<Match<Object, File>> matches = new ArrayList<Match<Object, File>>(model.matches());
|
|
|
|
StandardRenameAction action = (StandardRenameAction) getValue(RENAME_ACTION);
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-07 13:55:48 -04:00
|
|
|
// start processing
|
|
|
|
Map<File, File> renameLog = new LinkedHashMap<File, File>();
|
|
|
|
|
|
|
|
try {
|
2016-08-14 14:55:05 -04:00
|
|
|
if (useNativeShell() && NativeRenameAction.isSupported(action)) {
|
2016-05-07 13:55:48 -04:00
|
|
|
// call on EDT
|
2016-08-16 14:32:25 -04:00
|
|
|
NativeRenameWorker worker = new NativeRenameWorker(renameMap, renameLog, NativeRenameAction.valueOf(action.name()));
|
2016-05-07 13:55:48 -04:00
|
|
|
worker.call(null, null, null);
|
|
|
|
} else {
|
|
|
|
// call and wait
|
2016-08-16 14:32:25 -04:00
|
|
|
StandardRenameWorker worker = new StandardRenameWorker(renameMap, renameLog, action);
|
2016-05-09 16:28:32 -04:00
|
|
|
String message = String.format("%sing %d %s. This may take a while.", action.getDisplayName(), renameMap.size(), renameMap.size() == 1 ? "file" : "files");
|
2016-05-07 16:45:48 -04:00
|
|
|
ProgressMonitor.runTask(action.getDisplayName(), message, worker).get();
|
2016-04-14 05:32:03 -04:00
|
|
|
}
|
2016-05-07 13:55:48 -04:00
|
|
|
} catch (CancellationException e) {
|
|
|
|
debug.finest(e::toString);
|
|
|
|
} catch (Exception e) {
|
|
|
|
log.log(Level.SEVERE, String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)), e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// abort if nothing happened
|
|
|
|
if (renameLog.isEmpty()) {
|
|
|
|
return;
|
2012-07-17 16:52:03 -04:00
|
|
|
}
|
2014-01-08 12:23:04 -05:00
|
|
|
|
2016-05-07 13:55:48 -04:00
|
|
|
log.info(String.format("%d files renamed.", renameLog.size()));
|
|
|
|
|
|
|
|
// remove renamed matches
|
|
|
|
renameLog.forEach((from, to) -> {
|
|
|
|
model.matches().remove(model.files().indexOf(from));
|
|
|
|
});
|
|
|
|
|
|
|
|
HistorySpooler.getInstance().append(renameLog.entrySet());
|
|
|
|
|
2016-04-14 05:32:03 -04:00
|
|
|
// store xattr
|
|
|
|
storeMetaInfo(renameMap, matches);
|
2016-04-13 14:41:06 -04:00
|
|
|
|
2016-04-14 05:32:03 -04:00
|
|
|
// delete empty folders
|
|
|
|
if (action == StandardRenameAction.MOVE) {
|
2016-05-07 13:55:48 -04:00
|
|
|
deleteEmptyFolders(renameLog);
|
2016-04-14 05:32:03 -04:00
|
|
|
}
|
|
|
|
});
|
2012-07-19 23:38:47 -04:00
|
|
|
} catch (Throwable e) {
|
2016-03-09 11:02:36 -05:00
|
|
|
log.log(Level.WARNING, e.getMessage(), e);
|
2009-05-17 16:58:20 -04:00
|
|
|
}
|
2011-11-04 03:45:48 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-04-13 14:41:06 -04:00
|
|
|
private void storeMetaInfo(Map<File, File> renameMap, List<Match<Object, File>> matches) {
|
|
|
|
// write metadata into xattr if xattr is enabled
|
|
|
|
for (Match<Object, File> match : matches) {
|
|
|
|
File file = match.getCandidate();
|
2016-04-14 05:32:03 -04:00
|
|
|
Object info = match.getValue();
|
|
|
|
File destination = renameMap.get(file);
|
|
|
|
if (info != null && destination != null) {
|
|
|
|
destination = resolve(file, destination);
|
2016-04-13 14:41:06 -04:00
|
|
|
if (destination.isFile()) {
|
2016-04-14 05:32:03 -04:00
|
|
|
String original = file.getName();
|
|
|
|
debug.finest(format("Store xattr: [%s, %s] => %s", info, original, destination));
|
|
|
|
xattr.setMetaInfo(destination, info, original);
|
2016-04-13 14:41:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void deleteEmptyFolders(Map<File, File> renameMap) {
|
2016-04-14 05:32:03 -04:00
|
|
|
// collect empty folders and files in reverse order
|
2016-05-15 15:09:30 -04:00
|
|
|
Set<File> deleteFiles = new TreeSet<File>();
|
2016-04-13 14:41:06 -04:00
|
|
|
|
|
|
|
renameMap.forEach((s, d) -> {
|
|
|
|
File sourceFolder = s.getParentFile();
|
|
|
|
File destinationFolder = resolve(s, d).getParentFile();
|
|
|
|
|
2016-04-14 05:32:03 -04:00
|
|
|
// destination folder is the source, or is inside the source folder
|
2016-05-12 08:39:47 -04:00
|
|
|
if (d.getParentFile() == null || destinationFolder.getPath().startsWith(sourceFolder.getPath())) {
|
2016-04-14 05:32:03 -04:00
|
|
|
return;
|
|
|
|
}
|
2016-04-13 14:41:06 -04:00
|
|
|
|
2016-04-14 05:32:03 -04:00
|
|
|
try {
|
2016-05-06 07:39:00 -04:00
|
|
|
// guess affected folder depth
|
2016-05-12 08:39:47 -04:00
|
|
|
int tailSize = listStructurePathTail(d.getParentFile()).size();
|
2016-04-14 05:32:03 -04:00
|
|
|
|
2016-05-06 07:39:00 -04:00
|
|
|
for (int i = 0; i < tailSize && !isStructureRoot(sourceFolder); sourceFolder = sourceFolder.getParentFile(), i++) {
|
|
|
|
File[] children = sourceFolder.listFiles();
|
2016-05-15 15:09:30 -04:00
|
|
|
if (children == null || !stream(children).allMatch(f -> deleteFiles.contains(f) || isThumbnailStore(f))) {
|
2016-05-06 07:39:00 -04:00
|
|
|
return;
|
|
|
|
}
|
2016-04-14 05:32:03 -04:00
|
|
|
|
2016-05-15 15:09:30 -04:00
|
|
|
stream(children).forEach(deleteFiles::add);
|
|
|
|
deleteFiles.add(sourceFolder);
|
2016-05-06 07:39:00 -04:00
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
|
|
debug.warning(e::toString);
|
2016-04-13 14:41:06 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-05-07 06:14:46 -04:00
|
|
|
// use system trash to delete left-behind empty folders / hidden files
|
|
|
|
try {
|
2016-05-15 15:09:30 -04:00
|
|
|
for (File file : deleteFiles) {
|
|
|
|
if (file.exists()) {
|
2016-08-16 15:37:30 -04:00
|
|
|
UserFiles.trash(file);
|
2016-05-11 14:14:11 -04:00
|
|
|
}
|
|
|
|
}
|
2016-05-07 06:14:46 -04:00
|
|
|
} catch (Throwable e) {
|
|
|
|
debug.log(Level.WARNING, e, e::getMessage);
|
2016-04-13 14:41:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-16 15:37:30 -04:00
|
|
|
private Map<File, File> validate(Map<File, File> renameMap, Window parent) {
|
|
|
|
// rename map values as modifiable list
|
|
|
|
List<File> destinationPathView = new AbstractList<File>() {
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-08-16 15:37:30 -04:00
|
|
|
private File[] keyIndex = renameMap.keySet().toArray(new File[0]);
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2009-05-17 16:58:20 -04:00
|
|
|
@Override
|
2016-08-16 15:37:30 -04:00
|
|
|
public File get(int i) {
|
|
|
|
return renameMap.get(keyIndex[i]);
|
2009-05-17 16:58:20 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2009-05-17 16:58:20 -04:00
|
|
|
@Override
|
2016-08-16 15:37:30 -04:00
|
|
|
public File set(int i, File value) {
|
|
|
|
return renameMap.put(keyIndex[i], value);
|
2009-05-17 16:58:20 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2009-05-17 16:58:20 -04:00
|
|
|
@Override
|
|
|
|
public int size() {
|
2016-08-16 15:37:30 -04:00
|
|
|
return keyIndex.length;
|
2009-05-17 16:58:20 -04:00
|
|
|
}
|
|
|
|
};
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-08-16 15:37:30 -04:00
|
|
|
if (ValidateDialog.validate(parent, destinationPathView)) {
|
|
|
|
// ask for user permissions for output folders so we can check them
|
|
|
|
if (isMacSandbox()) {
|
|
|
|
if (!MacAppUtilities.askUnlockFolders(parent, renameMap.entrySet().stream().flatMap(e -> Stream.of(e.getKey(), resolve(e.getKey(), e.getValue()))).collect(toList()))) {
|
|
|
|
return emptyMap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ConflictDialog.check(parent, renameMap)) {
|
|
|
|
return renameMap;
|
|
|
|
}
|
2009-05-17 16:58:20 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2009-05-17 16:58:20 -04:00
|
|
|
// return empty list if validation was cancelled
|
2016-08-16 15:37:30 -04:00
|
|
|
return emptyMap();
|
2009-05-17 16:58:20 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
protected static class StandardRenameWorker implements ProgressWorker<Map<File, File>> {
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
private Map<File, File> renameMap;
|
|
|
|
private Map<File, File> renameLog;
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
private StandardRenameAction action;
|
2016-05-07 13:55:48 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
public StandardRenameWorker(Map<File, File> renameMap, Map<File, File> renameLog, StandardRenameAction action) {
|
2016-05-07 13:55:48 -04:00
|
|
|
this.renameMap = renameMap;
|
|
|
|
this.renameLog = renameLog;
|
2012-07-12 07:23:23 -04:00
|
|
|
this.action = action;
|
2011-11-04 03:45:48 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2011-11-04 03:45:48 -04:00
|
|
|
@Override
|
2016-05-07 13:55:48 -04:00
|
|
|
public Map<File, File> call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception {
|
|
|
|
for (Entry<File, File> mapping : renameMap.entrySet()) {
|
|
|
|
if (cancelled.get()) {
|
|
|
|
return renameLog;
|
2012-07-17 16:52:03 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-08 10:26:40 -04:00
|
|
|
message.accept(mapping.getKey().getName());
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-07 13:55:48 -04:00
|
|
|
// rename file, throw exception on failure
|
|
|
|
File source = mapping.getKey();
|
|
|
|
File destination = resolve(mapping.getKey(), mapping.getValue());
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-15 15:34:26 -04:00
|
|
|
if (!equalsCaseSensitive(source, destination)) {
|
2016-05-07 13:55:48 -04:00
|
|
|
action.rename(source, destination);
|
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-07 13:55:48 -04:00
|
|
|
// remember successfully renamed matches for history entry and possible revert
|
|
|
|
renameLog.put(mapping.getKey(), mapping.getValue());
|
2011-11-04 03:45:48 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-07 13:55:48 -04:00
|
|
|
return renameLog;
|
2011-11-04 03:45:48 -04:00
|
|
|
}
|
2012-07-17 16:52:03 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
protected static class NativeRenameWorker implements ProgressWorker<Map<File, File>> {
|
|
|
|
|
|
|
|
private Map<File, File> renameMap;
|
|
|
|
private Map<File, File> renameLog;
|
|
|
|
|
|
|
|
private NativeRenameAction action;
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2016-05-07 13:55:48 -04:00
|
|
|
public NativeRenameWorker(Map<File, File> renameMap, Map<File, File> renameLog, NativeRenameAction action) {
|
2016-08-16 14:32:25 -04:00
|
|
|
this.renameMap = renameMap;
|
|
|
|
this.renameLog = renameLog;
|
|
|
|
this.action = action;
|
2012-07-17 16:52:03 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2012-07-17 16:52:03 -04:00
|
|
|
@Override
|
2016-05-07 13:55:48 -04:00
|
|
|
public Map<File, File> call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception {
|
2013-03-23 08:34:15 -04:00
|
|
|
// prepare delta, ignore files already named as desired
|
2016-05-07 13:55:48 -04:00
|
|
|
Map<File, File> renamePlan = new LinkedHashMap<File, File>();
|
2016-08-14 14:55:05 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
renameMap.forEach((from, to) -> {
|
2016-08-14 14:55:05 -04:00
|
|
|
// resolve relative paths
|
2016-08-16 14:32:25 -04:00
|
|
|
to = resolve(from, to);
|
2016-08-14 14:55:05 -04:00
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
if (!equalsCaseSensitive(from, to)) {
|
|
|
|
renamePlan.put(from, to);
|
2013-03-23 08:34:15 -04:00
|
|
|
}
|
2016-08-14 14:55:05 -04:00
|
|
|
});
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2012-07-17 16:52:03 -04:00
|
|
|
// call native shell move/copy
|
|
|
|
try {
|
2016-08-16 14:32:25 -04:00
|
|
|
action.rename(renamePlan);
|
2012-07-17 16:52:03 -04:00
|
|
|
} catch (CancellationException e) {
|
2016-05-07 13:55:48 -04:00
|
|
|
debug.finest(e::getMessage);
|
|
|
|
}
|
|
|
|
|
2016-08-16 14:32:25 -04:00
|
|
|
// confirm results
|
|
|
|
renameMap.forEach((from, to) -> {
|
|
|
|
// resolve relative paths
|
|
|
|
if (resolve(from, to).exists()) {
|
|
|
|
renameLog.put(from, to);
|
2012-07-17 16:52:03 -04:00
|
|
|
}
|
2016-08-16 14:32:25 -04:00
|
|
|
});
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2012-07-17 16:52:03 -04:00
|
|
|
return renameLog;
|
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2011-11-04 03:45:48 -04:00
|
|
|
}
|
2013-09-30 00:46:33 -04:00
|
|
|
|
2007-12-23 14:28:04 -05:00
|
|
|
}
|