Reimplement coherent updating

This commit is contained in:
RatzzFatzz
2025-12-15 02:39:44 +01:00
parent 80c46508b8
commit a51922968e
13 changed files with 260 additions and 167 deletions

View File

@@ -0,0 +1,66 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class LoadFilesTest {
public static void main(String[] args) {
int depth = 2;
getDirectoriesAtDepth("/mnt/media/Anime", 2);
try (Stream<Path> paths = Files.walk(Paths.get("/mnt/media/Anime"), 2)) {
List<File> result = paths.map(Path::toFile)
.filter(File::isDirectory)
.collect(Collectors.toList());
System.out.println(result);
} catch (IOException e) {
}
}
private static List<File> getDirectoriesAtDepth(String path, int depth) {
List<File> result = new ArrayList<>();
File rootDir = Path.of(path).toFile();
if (!rootDir.exists()) throw new RuntimeException("Invalid path");
exploreDirectory(rootDir, 0, depth, result);
return result;
}
/**
* Recursively explores directories to find items at the target depth.
*
* @param currentDir The current directory being explored
* @param currentDepth The current depth level
* @param targetDepth The target depth to collect files
* @param result The collection to store found files
*/
private static void exploreDirectory(File currentDir, int currentDepth, int targetDepth, List<File> result) {
if (currentDepth == targetDepth) {
// We've reached the target depth, add this directory to results
result.add(currentDir);
return;
}
// Get all files and directories in the current directory
File[] files = currentDir.listFiles();
if (files == null) return;
// Recursively explore subdirectories
for (File file : files) {
if (file.isDirectory()) {
exploreDirectory(file, currentDepth + 1, targetDepth, result);
} else if (currentDepth + 1 == targetDepth) {
// If files at the next level would be at the target depth, include them
result.add(file);
}
}
}
}

View File

@@ -1,8 +1,8 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl; package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel.AttributeUpdaterKernel; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.AttributeUpdater;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel.CoherentAttributeUpdaterKernel; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.CoherentAttributeUpdater;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel.DefaultAttributeUpdaterKernel; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.SingleFileAttributeUpdater;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.CachedFileProcessor; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.CachedFileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.FileProcessor; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.FileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.MkvFileProcessor; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.MkvFileProcessor;
@@ -42,9 +42,9 @@ public class CommandRunner implements Runnable {
FileFilter fileFilter = new FileFilter(config.getExcluded(), config.getIncludePattern(), config.getFilterDate()); FileFilter fileFilter = new FileFilter(config.getExcluded(), config.getIncludePattern(), config.getFilterDate());
FileProcessor fileProcessor = new CachedFileProcessor(new MkvFileProcessor(config.getMkvToolNix(), fileFilter)); FileProcessor fileProcessor = new CachedFileProcessor(new MkvFileProcessor(config.getMkvToolNix(), fileFilter));
AttributeUpdaterKernel kernel = config.getCoherent() != null AttributeUpdater kernel = config.getCoherent() != null
? new CoherentAttributeUpdaterKernel(config, fileProcessor) ? new CoherentAttributeUpdater(config, fileProcessor)
: new DefaultAttributeUpdaterKernel(config, fileProcessor); : new SingleFileAttributeUpdater(config, fileProcessor);
kernel.execute(); kernel.execute();
} }
} }

View File

@@ -1,98 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.FileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class CoherentAttributeUpdaterKernel extends AttributeUpdaterKernel {
public CoherentAttributeUpdaterKernel(InputConfig config, FileProcessor processor) {
super(config, processor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" directories", 1);
}
List<File> loadFiles(String path, int depth) {
List<File> directories = processor.loadDirectories(path, depth)
.stream()
// .filter(file -> !excludedFiles.contains(file))
.collect(Collectors.toList());
return directories.stream()
.filter(dir -> isParentDirectory(dir, directories))
.collect(Collectors.toList());
}
private boolean isParentDirectory(File directory, List<File> directories) {
String path = directory.getAbsolutePath();
return directories.stream()
.noneMatch(dir -> dir.getAbsolutePath().contains(path) && !StringUtils.equals(path, dir.getAbsolutePath()));
}
/**
* Update files in directory, if possible, with the same {@link AttributeConfig}.
* If {@link InputConfig#isForceCoherent()} then there will be no changes to the file if they don't match the same config.
* Otherwise, the default behaviour is executed.
* This method is called by the executor and is run in parallel.
*
* @param file directory containing files
*/
@Override
void process(File file) {
process(file, config.getCoherent());
}
void process(File file, int depth) {
// TODO: Implement level crawl if coherence is not possible on user entered depth
// IMPL idea: recursive method call, cache needs to be implemented
// List<FileInfoOld> fileInfoOlds = collector.loadFiles(file.getAbsolutePath()).stream()
// .map(FileInfoOld::new)
// .collect(Collectors.toList());
for (AttributeConfig config : config.getAttributeConfig()) {
// for (FileInfoOld fileInfoOld : fileInfoOlds) {
// List<TrackAttributes> attributes = processor.readAttributes(fileInfoOld.getFile());
//
// List<TrackAttributes> nonForcedTracks = processor.retrieveNonForcedTracks(attributes);
// List<TrackAttributes> nonCommentaryTracks = processor.retrieveNonCommentaryTracks(attributes);
//
// processor.detectDefaultTracks(fileInfoOld, attributes, nonForcedTracks);
// processor.detectDesiredTracks(fileInfoOld, nonForcedTracks, nonCommentaryTracks, config);
// }
//
// if (fileInfoOlds.stream().allMatch(elem -> ("OFF".equals(config.getSubtitleLanguage()) || elem.getDesiredDefaultSubtitleLane() != null)
// && elem.getDesiredDefaultAudioLane() != null)) {
// log.info("Found {} match for {}", config.toStringShort(), file.getAbsolutePath());
// fileInfoOlds.forEach(this::updateFile);
// return; // match found, end process here
// }
// fileInfoOlds.forEach(f -> {
// f.setDesiredDefaultAudioLane(null);
// f.setDesiredDefaultSubtitleLane(null);
// });
}
log.info("No coherent match found for {}", file.getAbsoluteFile());
// for (FileInfoOld fileInfoOld : fileInfoOlds) {
// if (!InputConfig.getInstance().isForceCoherent()) {
// super.process(fileInfoOld.getFile());
// } else {
// statistic.excluded();
// }
// }
}
}

View File

@@ -1,20 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.FileProcessor;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
@Slf4j
public class DefaultAttributeUpdaterKernel extends AttributeUpdaterKernel {
public DefaultAttributeUpdaterKernel(InputConfig config, FileProcessor processor) {
super(config, processor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" files", 1);
}
}

View File

@@ -1,8 +1,6 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel; package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException; import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.AttributeProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.FileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
@@ -20,18 +18,18 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
public abstract class AttributeUpdaterKernel { public abstract class AttributeUpdater {
protected final InputConfig config; protected final InputConfig config;
protected final FileProcessor processor; protected final FileProcessor fileProcessor;
protected final AttributeProcessor attributeProcessor; protected final AttributeProcessor attributeProcessor;
protected final ResultStatistic statistic = ResultStatistic.getInstance(); protected final ResultStatistic statistic = ResultStatistic.getInstance();
private final ExecutorService executor; private final ExecutorService executor;
public AttributeUpdaterKernel(InputConfig config, FileProcessor processor) { public AttributeUpdater(InputConfig config, FileProcessor fileProcessor) {
this.config = config; this.config = config;
this.processor = processor; this.fileProcessor = fileProcessor;
this.attributeProcessor = new AttributeProcessor(config.getPreferredSubtitles().toArray(new String[0]), config.getForcedKeywords(), config.getCommentaryKeywords(), config.getHearingImpaired()); this.attributeProcessor = new AttributeProcessor(config.getPreferredSubtitles().toArray(new String[0]), config.getForcedKeywords(), config.getCommentaryKeywords(), config.getHearingImpaired());
this.executor = Executors.newFixedThreadPool(config.getThreads()); this.executor = Executors.newFixedThreadPool(config.getThreads());
} }
@@ -48,7 +46,9 @@ public abstract class AttributeUpdaterKernel {
statistic.startTimer(); statistic.startTimer();
try (ProgressBar progressBar = pbBuilder().build()) { try (ProgressBar progressBar = pbBuilder().build()) {
List<File> files = processor.loadFiles(config.getLibraryPath().getAbsolutePath()); List<File> files = config.getCoherent() != null
? fileProcessor.loadDirectory(config.getLibraryPath().getPath(), config.getCoherent())
: fileProcessor.loadFiles(config.getLibraryPath().getPath());
progressBar.maxHint(files.size()); progressBar.maxHint(files.size());
progressBar.refresh(); progressBar.refresh();
@@ -74,22 +74,7 @@ public abstract class AttributeUpdaterKernel {
* *
* @param file file or directory to update * @param file file or directory to update
*/ */
void process(File file) { protected abstract void process(File file);
FileInfo fileInfo = processor.readAttributes(file);
if (fileInfo.getTracks().isEmpty()) {
log.warn("No attributes found for file {}", file);
statistic.failure();
return;
}
attributeProcessor.findDefaultMatchAndApplyChanges(fileInfo, config.getAttributeConfig());
attributeProcessor.findForcedTracksAndApplyChanges(fileInfo, config.isOverwriteForced());
attributeProcessor.findCommentaryTracksAndApplyChanges(fileInfo);
attributeProcessor.findHearingImpairedTracksAndApplyChanges(fileInfo);
checkStatusAndUpdate(fileInfo);
}
/** /**
* Persist file changes. * Persist file changes.
@@ -119,12 +104,12 @@ public abstract class AttributeUpdaterKernel {
if (config.isSafeMode()) return; if (config.isSafeMode()) return;
try { try {
processor.update(fileInfo); fileProcessor.update(fileInfo);
statistic.success(); statistic.success();
log.info("Commited {} to '{}'", fileInfo.getMatchedConfig().toStringShort(), fileInfo.getFile().getAbsolutePath()); log.info("Commited {} to '{}'", fileInfo.getMatchedConfig().toStringShort(), fileInfo.getFile().getPath());
} catch (IOException | MkvToolNixException e) { } catch (IOException | MkvToolNixException e) {
statistic.failedChanging(); statistic.failedChanging();
log.warn("Couldn't commit {} to '{}'", fileInfo.getMatchedConfig().toStringShort(), fileInfo.getFile().getAbsoluteFile(), e); log.warn("Couldn't commit {} to '{}'", fileInfo.getMatchedConfig().toStringShort(), fileInfo.getFile().getPath(), e);
} }
} }

View File

@@ -25,8 +25,8 @@ public class CachedFileProcessor implements FileProcessor {
} }
@Override @Override
public List<File> loadDirectories(String path, int depth) { public List<File> loadDirectory(String path, int depth) {
return directoryCache.retrieve(Pair.of(path, depth), key -> processor.loadDirectories(key.getLeft(), key.getRight())); return directoryCache.retrieve(Pair.of(path, depth), key -> processor.loadDirectory(key.getLeft(), key.getRight()));
} }
@Override @Override

View File

@@ -0,0 +1,80 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
public class CoherentAttributeUpdater extends SingleFileAttributeUpdater {
public CoherentAttributeUpdater(InputConfig config, FileProcessor processor) {
super(config, processor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" directories", 1);
}
@Override
public void process(File rootDir) {
if (rootDir.isFile()) {
super.process(rootDir);
return;
}
List<File> files = fileProcessor.loadFiles(rootDir.getPath());
Set<FileInfo> matchedFiles = new HashSet<>(files.size() * 2);
AttributeConfig matchedConfig = null;
for (AttributeConfig config: config.getAttributeConfig()) {
for (File file: files) {
FileInfo fileInfo = fileProcessor.readAttributes(file);
fileInfo.resetChanges();
fileInfo.setMatchedConfig(null);
if (fileInfo.getTracks().isEmpty()) {
log.warn("No attributes found for file {}", file);
statistic.failure();
break;
}
attributeProcessor.findDefaultMatchAndApplyChanges(fileInfo, config);
if (matchedConfig == null) matchedConfig = fileInfo.getMatchedConfig();
matchedFiles.add(fileInfo);
if (matchedConfig != fileInfo.getMatchedConfig()) {
matchedConfig = null;
break;
}
}
if (matchedConfig != null) break;
}
if (matchedConfig != null) {
matchedFiles.forEach(fileInfo -> {
attributeProcessor.findForcedTracksAndApplyChanges(fileInfo, config.isOverwriteForced());
attributeProcessor.findCommentaryTracksAndApplyChanges(fileInfo);
attributeProcessor.findHearingImpairedTracksAndApplyChanges(fileInfo);
checkStatusAndUpdate(fileInfo);
});
} else {
log.info("No coherent match found, trying to find coherent match in child directories: {}", rootDir.getPath());
matchedFiles.forEach(fileInfo -> {
fileInfo.resetChanges();
fileInfo.setMatchedConfig(null);
});
for (File dir: fileProcessor.loadDirectory(rootDir.getPath(), 1)) this.process(dir);
}
}
}

View File

@@ -16,14 +16,16 @@ public interface FileProcessor {
List<File> loadFiles(String path); List<File> loadFiles(String path);
/** /**
* Load all directories from path, but only until depth is reached. * Load only directories and files at depth, ignoring everything between root dir and dir at depth.
* This will ignore all files between root and directories at depth. * E.g. with file structure /base/depth1/depth2/depth3.file
* - with depth 1: return /base/depth1/
* - with depth 2: returns /base/depth1/depth2/
* *
* @param path leads to a directory which will be loaded recursively until depth * @param path directory which will be loaded recursively until depth
* @param depth limit directory crawling * @param depth limit directory crawling
* @return list of directory until depth * @return list of directory at depth
*/ */
List<File> loadDirectories(String path, int depth); List<File> loadDirectory(String path, int depth);
/** /**
* Load track information from file. * Load track information from file.

View File

@@ -58,15 +58,45 @@ public class MkvFileProcessor implements FileProcessor {
*/ */
@Override @Override
// does this load /arst/arst & /arst ? // does this load /arst/arst & /arst ?
public List<File> loadDirectories(String path, int depth) { public List<File> loadDirectory(String path, int depth) {
try (Stream<Path> paths = Files.walk(Paths.get(path), depth)) { File rootDir = Path.of(path).toFile();
return paths.map(Path::toFile) if (!rootDir.exists()) {
.filter(File::isDirectory) log.error("Couldn't find file or directory!");
.collect(Collectors.toList());
} catch (IOException e) {
log.error("Couldn't find file or directory!", e);
return new ArrayList<>(); return new ArrayList<>();
} }
List<File> result = new ArrayList<>();
exploreDirectory(rootDir, 0, depth, result);
return result;
}
/**
* Recursively explores directories to find items at the target depth.
*
* @param currentDir The current directory being explored
* @param currentDepth The current depth level
* @param targetDepth The target depth to collect files
* @param result The collection to store found files
*/
private static void exploreDirectory(File currentDir, int currentDepth, int targetDepth, List<File> result) {
if (currentDepth == targetDepth) {
result.add(currentDir);
return;
}
// Get all files and directories in the current directory
File[] files = currentDir.listFiles();
if (files == null) return;
// Recursively explore subdirectories
for (File file : files) {
if (file.isDirectory()) {
exploreDirectory(file, currentDepth + 1, targetDepth, result);
} else if (currentDepth + 1 == targetDepth) {
// If files at the next level would be at the target depth, include them
result.add(file);
}
}
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@@ -0,0 +1,39 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import java.io.File;
@Slf4j
public class SingleFileAttributeUpdater extends AttributeUpdater {
public SingleFileAttributeUpdater(InputConfig config, FileProcessor processor) {
super(config, processor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" files", 1);
}
public void process(File file) {
FileInfo fileInfo = fileProcessor.readAttributes(file);
if (fileInfo.getTracks().isEmpty()) {
log.warn("No attributes found for file {}", file);
statistic.failure();
return;
}
attributeProcessor.findDefaultMatchAndApplyChanges(fileInfo, config.getAttributeConfig());
attributeProcessor.findForcedTracksAndApplyChanges(fileInfo, config.isOverwriteForced());
attributeProcessor.findCommentaryTracksAndApplyChanges(fileInfo);
attributeProcessor.findHearingImpairedTracksAndApplyChanges(fileInfo);
checkStatusAndUpdate(fileInfo);
}
}

View File

@@ -24,7 +24,7 @@ public class FileInfo {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final List<TrackAttributes> subtitleTracks = new ArrayList<>(); private final List<TrackAttributes> subtitleTracks = new ArrayList<>();
private final PlannedChange changes = new PlannedChange(); private PlannedChange changes = new PlannedChange();
@Setter @Setter
private AttributeConfig matchedConfig; private AttributeConfig matchedConfig;
@@ -50,6 +50,10 @@ public class FileInfo {
return Collections.unmodifiableList(subtitleTracks); return Collections.unmodifiableList(subtitleTracks);
} }
public void resetChanges() {
changes = new PlannedChange();
}
public FileStatus getStatus() { public FileStatus getStatus() {
if (!changes.isEmpty()) return FileStatus.CHANGE_NECESSARY; if (!changes.isEmpty()) return FileStatus.CHANGE_NECESSARY;
if (matchedConfig == null) return FileStatus.NO_SUITABLE_CONFIG; if (matchedConfig == null) return FileStatus.NO_SUITABLE_CONFIG;

View File

@@ -21,7 +21,7 @@ import picocli.CommandLine.Option;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@CommandLine.Command @CommandLine.Command
public class InputConfig { public class InputConfig implements CommandLine.IVersionProvider {
private File configPath; private File configPath;
@@ -108,5 +108,10 @@ public class InputConfig {
.add("attributeConfig=" + attributeConfig) .add("attributeConfig=" + attributeConfig)
.toString(); .toString();
} }
@Override
public String[] getVersion() throws Exception {
return new String[0];
}
} }