diff --git a/.idea/csv-editor.xml b/.idea/csv-editor.xml deleted file mode 100644 index 54fbf77..0000000 --- a/.idea/csv-editor.xml +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 4697f0e..21e87df 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -7,6 +7,8 @@ + + @@ -27,8 +29,14 @@ + + + + + + \ No newline at end of file diff --git a/.idea/sonarlint.xml b/.idea/sonarlint.xml index 8b17604..2645de5 100644 --- a/.idea/sonarlint.xml +++ b/.idea/sonarlint.xml @@ -4,6 +4,7 @@ diff --git a/api/src/main/java/com/github/gtache/autosubtitle/Translator.java b/api/src/main/java/com/github/gtache/autosubtitle/Translator.java index 2dceed1..ce7ebd5 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/Translator.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/Translator.java @@ -6,7 +6,7 @@ import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; /** * Translates texts and subtitles */ -public interface Translator { +public interface Translator { /** * Guesses the language of the given text @@ -42,7 +42,7 @@ public interface Translator { * @param to The target language * @return The translated subtitle */ - Subtitle translate(Subtitle subtitle, Language to); + T translate(Subtitle subtitle, Language to); /** * Translates the given subtitles collection to the given language @@ -51,5 +51,5 @@ public interface Translator { * @param to The target language * @return The translated subtitles collection */ - SubtitleCollection translate(SubtitleCollection collection, Language to); + SubtitleCollection translate(SubtitleCollection collection, Language to); } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java b/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java index a594658..f19bc7c 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java @@ -19,7 +19,7 @@ public interface VideoConverter { * @return The modified video * @throws IOException If an I/O error occurs */ - Video addSoftSubtitles(final Video video, final Collection subtitles) throws IOException; + Video addSoftSubtitles(final Video video, final Collection> subtitles) throws IOException; /** * Adds soft subtitles to the given video @@ -29,7 +29,7 @@ public interface VideoConverter { * @param path The output path * @throws IOException If an I/O error occurs */ - void addSoftSubtitles(final Video video, final Collection subtitles, final Path path) throws IOException; + void addSoftSubtitles(final Video video, final Collection> subtitles, final Path path) throws IOException; /** * Adds hard subtitles to the given video @@ -39,7 +39,7 @@ public interface VideoConverter { * @return The modified video * @throws IOException If an I/O error occurs */ - Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException; + Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException; /** * Adds hard subtitles to the given video @@ -49,7 +49,7 @@ public interface VideoConverter { * @param path The output path * @throws IOException If an I/O error occurs */ - void addHardSubtitles(final Video video, final SubtitleCollection subtitles, final Path path) throws IOException; + void addHardSubtitles(final Video video, final SubtitleCollection subtitles, final Path path) throws IOException; /** * Extracts the audio from the given video diff --git a/api/src/main/java/com/github/gtache/autosubtitle/archive/Archiver.java b/api/src/main/java/com/github/gtache/autosubtitle/archive/Archiver.java new file mode 100644 index 0000000..381f350 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/archive/Archiver.java @@ -0,0 +1,44 @@ +package com.github.gtache.autosubtitle.archive; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +/** + * Compresses and uncompresses files + */ +public interface Archiver { + + /** + * Zips multiple files to the given destination + * + * @param files The files to zip + * @param destination The zipped file + * @throws IOException if an error occurs + */ + void compress(final List files, final Path destination) throws IOException; + + /** + * Unzips an archive to the given destination + * + * @param archive The archive + * @param destination The destination folder + * @throws IOException if an error occurs + */ + void decompress(final Path archive, final Path destination) throws IOException; + + /** + * Checks whether the given archive file is supported by the compresser + * + * @param path The file path + * @return True if the file is supported + */ + default boolean isPathSupported(final Path path) { + return path.toString().endsWith("." + archiveExtension()); + } + + /** + * @return The zipped archive extension + */ + String archiveExtension(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessListener.java b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessListener.java index 0e58649..a3a1cdf 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessListener.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessListener.java @@ -22,10 +22,11 @@ public interface ProcessListener { String readLine() throws IOException; /** - * Waits for the process to finish + * Waits for the process to finish. Automatically reads the process output to ensure it doesn't get stuck. * * @param duration The maximum time to wait * @return The process result + * @throws IOException if an error occurs */ ProcessResult join(final Duration duration) throws IOException; } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java index cfbddca..5d15a4a 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java @@ -1,6 +1,7 @@ package com.github.gtache.autosubtitle.process; import java.io.IOException; +import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -10,24 +11,25 @@ import java.util.List; public interface ProcessRunner { /** - * Runs a command + * Runs a command and waits max 1 hour for the process to run * * @param args the command * @return the result * @throws IOException if something goes wrong */ default ProcessResult run(final String... args) throws IOException { - return run(Arrays.asList(args)); + return run(Arrays.asList(args), Duration.ofHours(1)); } /** * Runs a command * - * @param args the command + * @param args the command + * @param duration The maximum duration to wait for * @return the result * @throws IOException if something goes wrong */ - ProcessResult run(final List args) throws IOException; + ProcessResult run(final List args, final Duration duration) throws IOException; /** * Starts a process diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java index 929042c..417ae97 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java @@ -7,7 +7,7 @@ import java.util.Collection; /** * Represents a collection of {@link Subtitle} */ -public interface SubtitleCollection { +public interface SubtitleCollection { /** * @return The whole text of the subtitles @@ -17,7 +17,7 @@ public interface SubtitleCollection { /** * @return The subtitles */ - Collection subtitles(); + Collection subtitles(); /** * @return The language of the subtitles diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleImporterExporter.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleImporterExporter.java new file mode 100644 index 0000000..8fa222f --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleImporterExporter.java @@ -0,0 +1,56 @@ +package com.github.gtache.autosubtitle.subtitle; + +import com.github.gtache.autosubtitle.Language; +import com.github.gtache.autosubtitle.subtitle.converter.ParseException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Imports and exports subtitles + */ +public interface SubtitleImporterExporter { + + /** + * Imports subtitles from a file + * + * @param file The path to the file + * @return A mapping of langauge to collection + * @throws IOException If an error occurred + * @throws ParseException If an error occurred while parsing a subtitle + */ + Map> importSubtitles(final Path file) throws IOException, ParseException; + + /** + * Exports multiple collections to a file + * + * @param collections The subtitle collections + * @param file The path to the file + * @throws IOException If an error occurred + */ + void exportSubtitles(final Collection> collections, final Path file) throws IOException; + + /** + * Exports a single collection to a file + * + * @param collection The subtitle collection + * @param file The path to the file + * @throws IOException If an error occurred + */ + default void exportSubtitles(final SubtitleCollection collection, final Path file) throws IOException { + exportSubtitles(List.of(collection), file); + } + + /** + * @return The supported file extensions for multiple collection exports + */ + Collection supportedArchiveExtensions(); + + /** + * @return The supported file extensions for single collection exports + */ + Collection supportedSingleFileExtensions(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/SubtitleConverter.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/SubtitleConverter.java index c926884..2b70487 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/SubtitleConverter.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/SubtitleConverter.java @@ -1,5 +1,6 @@ package com.github.gtache.autosubtitle.subtitle.converter; +import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import java.io.IOException; @@ -9,7 +10,7 @@ import java.nio.file.Path; /** * Converts subtitles to a specific format (e.g. srt, ssa, ass, ...) and vice-versa */ -public interface SubtitleConverter { +public interface SubtitleConverter { /** * Converts the subtitle collection @@ -17,7 +18,7 @@ public interface SubtitleConverter { * @param collection The collection * @return The converted subtitles as the content of a file */ - String format(final SubtitleCollection collection); + String format(final SubtitleCollection collection); /** * Parses a subtitle collection @@ -26,7 +27,7 @@ public interface SubtitleConverter { * @return The subtitle collection * @throws ParseException If an error occurred */ - default SubtitleCollection parse(final Path file) throws ParseException { + default SubtitleCollection parse(final Path file) throws ParseException { try { final var content = Files.readString(file); return parse(content); @@ -42,7 +43,7 @@ public interface SubtitleConverter { * @return The subtitle collection * @throws ParseException If an error occurred */ - SubtitleCollection parse(String content) throws ParseException; + SubtitleCollection parse(String content) throws ParseException; /** * Check if the parser can parse the given file diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/extractor/SubtitleExtractor.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/extractor/SubtitleExtractor.java index fd91d11..f5e77bf 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/extractor/SubtitleExtractor.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/extractor/SubtitleExtractor.java @@ -3,12 +3,13 @@ package com.github.gtache.autosubtitle.subtitle.extractor; import com.github.gtache.autosubtitle.Audio; import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; /** * Extracts subtitles from a video or audio */ -public interface SubtitleExtractor { +public interface SubtitleExtractor { /** * Adds a listener @@ -37,7 +38,7 @@ public interface SubtitleExtractor { * @return The extracted subtitle collection * @throws ExtractException If an error occurs */ - default SubtitleCollection extract(final Video video, final ExtractionModel model) throws ExtractException { + default SubtitleCollection extract(final Video video, final ExtractionModel model) throws ExtractException { return extract(video, Language.AUTO, model); } @@ -50,7 +51,7 @@ public interface SubtitleExtractor { * @return The extracted subtitle collection * @throws ExtractException If an error occurs */ - SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException; + SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException; /** * Extracts the subtitles from an audio @@ -60,7 +61,7 @@ public interface SubtitleExtractor { * @return The extracted subtitle collection * @throws ExtractException If an error occurs */ - default SubtitleCollection extract(final Audio audio, final ExtractionModel model) throws ExtractException { + default SubtitleCollection extract(final Audio audio, final ExtractionModel model) throws ExtractException { return extract(audio, Language.AUTO, model); } @@ -73,5 +74,5 @@ public interface SubtitleExtractor { * @return The extracted subtitle collection * @throws ExtractException If an error occurs */ - SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException; + SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException; } diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index c19dfaa..2a4c54f 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -3,6 +3,7 @@ */ module com.github.gtache.autosubtitle.api { exports com.github.gtache.autosubtitle; + exports com.github.gtache.autosubtitle.archive; exports com.github.gtache.autosubtitle.process; exports com.github.gtache.autosubtitle.setup; exports com.github.gtache.autosubtitle.subtitle; diff --git a/api/src/test/java/com/github/gtache/autosubtitle/com/github/gtache/autosubtitle/subtitle/converter/TestSubtitleConverter.java b/api/src/test/java/com/github/gtache/autosubtitle/com/github/gtache/autosubtitle/subtitle/converter/TestSubtitleConverter.java index 1c47de8..54d95ee 100644 --- a/api/src/test/java/com/github/gtache/autosubtitle/com/github/gtache/autosubtitle/subtitle/converter/TestSubtitleConverter.java +++ b/api/src/test/java/com/github/gtache/autosubtitle/com/github/gtache/autosubtitle/subtitle/converter/TestSubtitleConverter.java @@ -1,5 +1,6 @@ package com.github.gtache.autosubtitle.com.github.gtache.autosubtitle.subtitle.converter; +import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.converter.ParseException; import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; @@ -23,10 +24,10 @@ import static org.mockito.Mockito.when; class TestSubtitleConverter { private final SubtitleConverter subtitleConverter; - private final SubtitleCollection subtitleCollection; + private final SubtitleCollection subtitleCollection; TestSubtitleConverter(@Mock final SubtitleConverter subtitleConverter, - @Mock final SubtitleCollection subtitleCollection) { + @Mock final SubtitleCollection subtitleCollection) { this.subtitleConverter = Objects.requireNonNull(subtitleConverter); this.subtitleCollection = Objects.requireNonNull(subtitleCollection); } diff --git a/api/src/test/java/com/github/gtache/autosubtitle/process/TestProcessRunner.java b/api/src/test/java/com/github/gtache/autosubtitle/process/TestProcessRunner.java index 579f1ac..a6389f1 100644 --- a/api/src/test/java/com/github/gtache/autosubtitle/process/TestProcessRunner.java +++ b/api/src/test/java/com/github/gtache/autosubtitle/process/TestProcessRunner.java @@ -7,6 +7,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; +import java.time.Duration; import java.util.List; import java.util.Objects; @@ -32,7 +33,7 @@ class TestProcessRunner { @Test void testRunVarargs() throws IOException { - when(processRunner.run(List.of("arg1", "arg2"))).thenReturn(result); + when(processRunner.run(List.of("arg1", "arg2"), Duration.ofHours(1))).thenReturn(result); when(processRunner.run(any(String[].class))).thenCallRealMethod(); assertEquals(result, processRunner.run("arg1", "arg2")); diff --git a/cli/pom.xml b/cli/pom.xml index 70e0d72..f4e4df9 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -22,7 +22,7 @@ com.github.gtache.autosubtitle - autosubtitle-whisper + autosubtitle-whisperx org.apache.logging.log4j diff --git a/cli/src/main/java/com/github/gtache/autosubtitle/modules/cli/CliComponent.java b/cli/src/main/java/com/github/gtache/autosubtitle/modules/cli/CliComponent.java index 9ca64da..797a2b7 100644 --- a/cli/src/main/java/com/github/gtache/autosubtitle/modules/cli/CliComponent.java +++ b/cli/src/main/java/com/github/gtache/autosubtitle/modules/cli/CliComponent.java @@ -3,14 +3,14 @@ package com.github.gtache.autosubtitle.modules.cli; import com.github.gtache.autosubtitle.modules.deepl.DeepLModule; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule; import com.github.gtache.autosubtitle.modules.impl.CoreModule; -import com.github.gtache.autosubtitle.modules.subtitle.extractor.whisper.WhisperExtractorModule; +import com.github.gtache.autosubtitle.modules.whisperx.WhisperXModule; import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; import dagger.Component; import javax.inject.Singleton; import java.util.Map; -@Component(modules = {CoreModule.class, DeepLModule.class, FFmpegModule.class, WhisperExtractorModule.class}) +@Component(modules = {CoreModule.class, DeepLModule.class, FFmpegModule.class, WhisperXModule.class}) @Singleton public interface CliComponent { diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java index 74a5d6b..ec5b9fa 100644 --- a/cli/src/main/java/module-info.java +++ b/cli/src/main/java/module-info.java @@ -4,6 +4,6 @@ module com.github.gtache.autosubtitle.cli { requires com.github.gtache.autosubtitle.deepl; requires com.github.gtache.autosubtitle.ffmpeg; - requires com.github.gtache.autosubtitle.whisper; + requires com.github.gtache.autosubtitle.whisperx; requires info.picocli; } \ No newline at end of file diff --git a/conda/pom.xml b/conda/pom.xml new file mode 100644 index 0000000..535eabc --- /dev/null +++ b/conda/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.github.gtache.autosubtitle + autosubtitle + 1.0-SNAPSHOT + + + autosubtitle-conda + + + + com.github.gtache.autosubtitle + autosubtitle-core + + + \ No newline at end of file diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaBundledPath.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaBundledPath.java similarity index 86% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaBundledPath.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaBundledPath.java index 2d80b90..d3dd0fc 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaBundledPath.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaBundledPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaInstallerPath.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaInstallerPath.java similarity index 86% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaInstallerPath.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaInstallerPath.java index a514484..6a536d8 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaInstallerPath.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaInstallerPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaMinimumMajorVersion.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaMinimumMajorVersion.java similarity index 86% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaMinimumMajorVersion.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaMinimumMajorVersion.java index 654f0b6..b391b1e 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaMinimumMajorVersion.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaMinimumMajorVersion.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaMinimumMinorVersion.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaMinimumMinorVersion.java similarity index 86% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaMinimumMinorVersion.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaMinimumMinorVersion.java index e89879b..857f5a2 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaMinimumMinorVersion.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaMinimumMinorVersion.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaRootPath.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaRootPath.java similarity index 86% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaRootPath.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaRootPath.java index b8da150..1ce8230 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaRootPath.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaRootPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaSetupModule.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaSetupModule.java similarity index 80% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaSetupModule.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaSetupModule.java index e92a78e..757f94b 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaSetupModule.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaSetupModule.java @@ -1,8 +1,10 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import com.github.gtache.autosubtitle.impl.Architecture; import com.github.gtache.autosubtitle.impl.OS; -import com.github.gtache.autosubtitle.setup.whisper.CondaSetupConfiguration; +import com.github.gtache.autosubtitle.modules.setup.impl.CacheRoot; +import com.github.gtache.autosubtitle.modules.setup.impl.ToolsRoot; +import com.github.gtache.autosubtitle.setup.conda.CondaSetupConfiguration; import dagger.Module; import dagger.Provides; @@ -56,13 +58,13 @@ public final class CondaSetupModule { @Provides @CondaRootPath - static Path providesCondaRootPath(@WhisperBundledRoot final Path root, final OS os) { + static Path providesCondaRootPath(@ToolsRoot final Path root) { return root.resolve(MINICONDA3); } @Provides @CondaInstallerPath - static Path providesCondaInstallerPath(@WhisperBundledRoot final Path root, final OS os) { - return root.resolve("cache").resolve("conda-install" + (os == OS.WINDOWS ? ".exe" : ".sh")); + static Path providesCondaInstallerPath(@CacheRoot final Path root, final OS os) { + return root.resolve("conda-install" + (os == OS.WINDOWS ? ".exe" : ".sh")); } } diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaSystemPath.java b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaSystemPath.java similarity index 86% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaSystemPath.java rename to conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaSystemPath.java index e819bd2..a0ab21c 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/CondaSystemPath.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/modules/setup/conda/CondaSystemPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.conda; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/setup/whisper/CondaSetupConfiguration.java b/conda/src/main/java/com/github/gtache/autosubtitle/setup/conda/CondaSetupConfiguration.java similarity index 95% rename from whisper/src/main/java/com/github/gtache/autosubtitle/setup/whisper/CondaSetupConfiguration.java rename to conda/src/main/java/com/github/gtache/autosubtitle/setup/conda/CondaSetupConfiguration.java index 6134033..698cc2d 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/setup/whisper/CondaSetupConfiguration.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/setup/conda/CondaSetupConfiguration.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.setup.whisper; +package com.github.gtache.autosubtitle.setup.conda; import com.github.gtache.autosubtitle.impl.Architecture; import com.github.gtache.autosubtitle.impl.OS; diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/setup/whisper/CondaSetupManager.java b/conda/src/main/java/com/github/gtache/autosubtitle/setup/conda/CondaSetupManager.java similarity index 89% rename from whisper/src/main/java/com/github/gtache/autosubtitle/setup/whisper/CondaSetupManager.java rename to conda/src/main/java/com/github/gtache/autosubtitle/setup/conda/CondaSetupManager.java index 534d41d..3b670b5 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/setup/whisper/CondaSetupManager.java +++ b/conda/src/main/java/com/github/gtache/autosubtitle/setup/conda/CondaSetupManager.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.setup.whisper; +package com.github.gtache.autosubtitle.setup.conda; import com.github.gtache.autosubtitle.impl.OS; import com.github.gtache.autosubtitle.setup.SetupException; @@ -13,7 +13,9 @@ import java.io.IOException; import java.net.http.HttpClient; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import static java.util.Objects.requireNonNull; @@ -67,8 +69,8 @@ public class CondaSetupManager extends AbstractSetupManager { logger.info("Conda downloaded"); } switch (configuration.os()) { - case WINDOWS -> installWindows(); - case MAC, LINUX -> installLinux(); + case OS.WINDOWS -> installWindows(); + case OS.MAC, OS.LINUX -> installLinux(); } } @@ -77,7 +79,7 @@ public class CondaSetupManager extends AbstractSetupManager { final var installerPath = configuration.condaInstallerPath(); final var rootPath = configuration.condaRootPath(); logger.info("Installing conda using {}", installerPath); - final var result = run("bash", installerPath.toString(), "-b", "-p", rootPath.toString()); + final var result = run(List.of("bash", installerPath.toString(), "-b", "-p", rootPath.toString()), Duration.ofMinutes(15)); if (result.exitCode() == 0) { logger.info("Installed conda to {}", rootPath); } else { @@ -93,7 +95,7 @@ public class CondaSetupManager extends AbstractSetupManager { final var installerPath = configuration.condaInstallerPath(); final var rootPath = configuration.condaRootPath(); logger.info("Installing conda using {}", installerPath); - final var result = run(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + rootPath.toString()); + final var result = run(List.of(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + rootPath.toString()), Duration.ofMinutes(15)); if (result.exitCode() == 0) { logger.info("Installed conda to {}", rootPath); } else { @@ -106,9 +108,9 @@ public class CondaSetupManager extends AbstractSetupManager { private void downloadConda() throws SetupException { switch (configuration.os()) { - case WINDOWS -> downloadCondaWindows(); - case MAC -> downloadCondaMac(); - case LINUX -> downloadCondaLinux(); + case OS.WINDOWS -> downloadCondaWindows(); + case OS.MAC -> downloadCondaMac(); + case OS.LINUX -> downloadCondaLinux(); } logger.info("Downloaded conda to {}", configuration.condaInstallerPath()); } @@ -152,7 +154,7 @@ public class CondaSetupManager extends AbstractSetupManager { @Override public void update() throws SetupException { try { - final var result = run(getCondaPath().toString(), "update", "-y", "conda"); + final var result = run(List.of(getCondaPath().toString(), "update", "-y", "conda"), Duration.ofMinutes(15)); if (result.exitCode() == 0) { logger.info("Conda updated"); } else { @@ -167,7 +169,7 @@ public class CondaSetupManager extends AbstractSetupManager { final var args = Stream.concat(Stream.of(getCondaPath().toString(), "create", "-y", "-p", path.toString(), "python=" + pythonVersion), Arrays.stream(packages)).toList(); try { logger.info("Creating venv {}", path); - final var result = run(args); + final var result = run(args, Duration.ofMinutes(15)); if (result.exitCode() == 0) { logger.info("Created venv {}", path); } else { @@ -200,7 +202,7 @@ public class CondaSetupManager extends AbstractSetupManager { private boolean isSystemCondaInstalled() throws SetupException { try { - final var result = run(configuration.condaSystemPath().toString(), "--version"); + final var result = run(List.of(configuration.condaSystemPath().toString(), "--version"), Duration.ofSeconds(5)); if (result.exitCode() == 0) { final var output = result.output().getFirst(); final var versionString = output.substring(output.indexOf(' ') + 1); diff --git a/conda/src/main/java/module-info.java b/conda/src/main/java/module-info.java new file mode 100644 index 0000000..3bc7f1d --- /dev/null +++ b/conda/src/main/java/module-info.java @@ -0,0 +1,10 @@ +/** + * Module for conda + */ +module com.github.gtache.autosubtitle.conda { + requires transitive com.github.gtache.autosubtitle.core; + requires org.apache.logging.log4j; + + exports com.github.gtache.autosubtitle.setup.conda; + exports com.github.gtache.autosubtitle.modules.setup.conda; +} \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index ee2e5d0..03c549d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -24,5 +24,10 @@ org.apache.logging.log4j log4j-api + + org.apache.logging.log4j + log4j-core + test + \ No newline at end of file diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/ZipDecompresser.java b/core/src/main/java/com/github/gtache/autosubtitle/archive/impl/ZipDecompresser.java similarity index 76% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/ZipDecompresser.java rename to core/src/main/java/com/github/gtache/autosubtitle/archive/impl/ZipDecompresser.java index 931d9e0..4c0625d 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/ZipDecompresser.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/archive/impl/ZipDecompresser.java @@ -1,16 +1,31 @@ -package com.github.gtache.autosubtitle.setup.ffmpeg; +package com.github.gtache.autosubtitle.archive.impl; +import com.github.gtache.autosubtitle.archive.Archiver; + +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** - * Zip implementation of {@link Decompresser} + * Zip implementation of {@link Archiver} */ -public class ZipDecompresser implements Decompresser { +public class ZipDecompresser implements Archiver { + + @Inject + ZipDecompresser() { + + } + + @Override + public void compress(final List files, final Path destination) throws IOException { + throw new UnsupportedOperationException("Not supported"); + } + @Override public void decompress(final Path archive, final Path destination) throws IOException { if (!isPathSupported(archive)) { @@ -49,9 +64,9 @@ public class ZipDecompresser implements Decompresser { } return destPath; } - + @Override - public boolean isPathSupported(final Path path) { - return path.getFileName().toString().endsWith(".zip"); + public String archiveExtension() { + return "zip"; } } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/modules/impl/CoreModule.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/impl/CoreModule.java index 1894b9a..c7736d5 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/modules/impl/CoreModule.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/impl/CoreModule.java @@ -1,23 +1,33 @@ package com.github.gtache.autosubtitle.modules.impl; +import com.github.gtache.autosubtitle.archive.Archiver; +import com.github.gtache.autosubtitle.archive.impl.ZipDecompresser; import com.github.gtache.autosubtitle.impl.Architecture; import com.github.gtache.autosubtitle.impl.DaggerException; import com.github.gtache.autosubtitle.impl.OS; import com.github.gtache.autosubtitle.modules.setup.impl.SetupModule; import com.github.gtache.autosubtitle.modules.subtitle.impl.SubtitleModule; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.IntoMap; +import dagger.multibindings.StringKey; /** * Dagger module for Core */ @Module(includes = {SetupModule.class, SubtitleModule.class}) -public final class CoreModule { +public abstract class CoreModule { private CoreModule() { } + @Binds + @StringKey("zip") + @IntoMap + abstract Archiver bindsZipDecompresser(final ZipDecompresser decompresser); + @Provides static OS providesOS() { final var os = OS.getOS(); diff --git a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/WhisperVersion.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/CacheRoot.java similarity index 79% rename from whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/WhisperVersion.java rename to core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/CacheRoot.java index 96f6817..e454127 100644 --- a/whisper/src/main/java/com/github/gtache/autosubtitle/modules/setup/whisper/WhisperVersion.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/CacheRoot.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.setup.whisper; +package com.github.gtache.autosubtitle.modules.setup.impl; import javax.inject.Qualifier; import java.lang.annotation.Documented; @@ -12,5 +12,5 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Documented @Retention(RUNTIME) @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) -public @interface WhisperVersion { +public @interface CacheRoot { } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SetupModule.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SetupModule.java index 0280791..767d454 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SetupModule.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SetupModule.java @@ -4,6 +4,8 @@ import dagger.Module; import dagger.Provides; import java.net.http.HttpClient; +import java.nio.file.Path; +import java.nio.file.Paths; /** * Dagger core module for setup @@ -14,6 +16,18 @@ public final class SetupModule { private SetupModule() { } + @Provides + @CacheRoot + static Path providesCacheRoot() { + return Paths.get("cache"); + } + + @Provides + @ToolsRoot + static Path providesToolsRoot() { + return Paths.get("tools"); + } + @Provides static HttpClient providesHttpClient() { return HttpClient.newHttpClient(); diff --git a/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/ToolsRoot.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/ToolsRoot.java new file mode 100644 index 0000000..5fb97fe --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/ToolsRoot.java @@ -0,0 +1,16 @@ +package com.github.gtache.autosubtitle.modules.setup.impl; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) +public @interface ToolsRoot { +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/modules/subtitle/impl/SubtitleModule.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/subtitle/impl/SubtitleModule.java index fc04deb..2a4e256 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/modules/subtitle/impl/SubtitleModule.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/subtitle/impl/SubtitleModule.java @@ -1,7 +1,9 @@ package com.github.gtache.autosubtitle.modules.subtitle.impl; +import com.github.gtache.autosubtitle.subtitle.SubtitleImporterExporter; import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; import com.github.gtache.autosubtitle.subtitle.converter.impl.SRTSubtitleConverter; +import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImporterExporterImpl; import dagger.Binds; import dagger.Module; import dagger.multibindings.IntoMap; @@ -20,4 +22,7 @@ public abstract class SubtitleModule { @IntoMap @StringKey("srt") abstract SubtitleConverter bindsSubtitleConverter(final SRTSubtitleConverter converter); + + @Binds + abstract SubtitleImporterExporter bindsSubtitleImporterExporter(final SubtitleImporterExporterImpl impl); } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java index 7fa315f..870c560 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java @@ -9,7 +9,6 @@ import org.apache.logging.log4j.Logger; import java.io.IOException; import java.time.Duration; import java.util.List; -import java.util.concurrent.CompletableFuture; /** * Base implementation of {@link ProcessRunner} @@ -19,19 +18,9 @@ public abstract class AbstractProcessRunner implements ProcessRunner { private static final Logger logger = LogManager.getLogger(AbstractProcessRunner.class); @Override - public ProcessResult run(final List args) throws IOException { + public ProcessResult run(final List args, final Duration duration) throws IOException { final var listener = startListen(args); - CompletableFuture.runAsync(() -> { - try { - var line = listener.readLine(); - while (line != null) { - line = listener.readLine(); - } - } catch (final IOException e) { - logger.error("Error listening to process output of {}", args, e); - } - }); - return listener.join(Duration.ofHours(1)); + return listener.join(duration); } @Override @@ -51,11 +40,12 @@ public abstract class AbstractProcessRunner implements ProcessRunner { /** * Runs a process and writes the output to the log * - * @param args the command + * @param args the command + * @param duration The maximum duration to wait for * @return the result * @throws IOException if an error occurs */ - protected ProcessResult runListen(final List args) throws IOException { + protected ProcessResult runListen(final List args, final Duration duration) throws IOException { final var listener = startListen(args); var line = listener.readLine(); final var processName = args.getFirst(); @@ -63,6 +53,6 @@ public abstract class AbstractProcessRunner implements ProcessRunner { logger.info("[{}]: {}", processName, line); line = listener.readLine(); } - return listener.join(Duration.ofHours(1)); + return listener.join(duration); } } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessListenerImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessListenerImpl.java index af668ff..b067a11 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessListenerImpl.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessListenerImpl.java @@ -2,22 +2,25 @@ package com.github.gtache.autosubtitle.process.impl; import com.github.gtache.autosubtitle.process.ProcessListener; import com.github.gtache.autosubtitle.process.ProcessResult; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; /** * Implementation of {@link ProcessListener} */ public class ProcessListenerImpl implements ProcessListener { - + private static final Logger logger = LogManager.getLogger(ProcessListenerImpl.class); private final Process process; private final BufferedReader reader; private final List output; @@ -30,7 +33,7 @@ public class ProcessListenerImpl implements ProcessListener { public ProcessListenerImpl(final Process process) { this.process = Objects.requireNonNull(process); this.reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); - this.output = new ArrayList<>(); + this.output = new CopyOnWriteArrayList<>(); } @Override @@ -48,7 +51,24 @@ public class ProcessListenerImpl implements ProcessListener { } @Override - public ProcessResult join(final Duration duration) throws IOException { + public ProcessResult join(final Duration duration) { + //Read to ensure process doesn't get stuck + CompletableFuture.runAsync(() -> { + try { + var line = readLine(); + while (line != null) { + line = readLine(); + } + } catch (final IOException e) { + logger.error("Error listening to process output of {}", process, e); + } finally { + try { + reader.close(); + } catch (final IOException e) { + logger.warn("Error closing reader of {}", process, e); + } + } + }); try { process.waitFor(duration.getSeconds(), TimeUnit.SECONDS); } catch (final InterruptedException e) { @@ -58,11 +78,6 @@ public class ProcessListenerImpl implements ProcessListener { if (process.isAlive()) { process.destroyForcibly(); } - //Reads lines to output - while (readLine() != null) { - //Do nothing - } - reader.close(); return new ProcessResultImpl(process.exitValue(), output); } } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/impl/SRTSubtitleConverter.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/impl/SRTSubtitleConverter.java index bf300a8..16f4304 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/impl/SRTSubtitleConverter.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/converter/impl/SRTSubtitleConverter.java @@ -9,7 +9,6 @@ import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl; import com.github.gtache.autosubtitle.subtitle.impl.SubtitleImpl; import javax.inject.Inject; -import javax.inject.Singleton; import java.util.Arrays; import java.util.Comparator; import java.util.stream.Collectors; @@ -20,17 +19,17 @@ import static java.util.Objects.requireNonNull; /** * Converts subtitles to SRT format */ -@Singleton -public class SRTSubtitleConverter implements SubtitleConverter { +public class SRTSubtitleConverter implements SubtitleConverter { - private final Translator translator; + private final Translator translator; @Inject SRTSubtitleConverter(final Translator translator) { this.translator = requireNonNull(translator); } - public String format(final SubtitleCollection collection) { + @Override + public String format(final SubtitleCollection collection) { final var subtitles = collection.subtitles().stream().sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList(); return IntStream.range(0, subtitles.size()).mapToObj(i -> { final var subtitle = subtitles.get(i); @@ -51,7 +50,7 @@ public class SRTSubtitleConverter implements SubtitleConverter { } @Override - public SubtitleCollection parse(final String content) throws ParseException { + public SubtitleCollectionImpl parse(final String content) throws ParseException { try { final var elements = content.split("\n\n"); final var subtitles = Arrays.stream(elements).filter(element -> !element.isBlank()).map(element -> { @@ -66,7 +65,7 @@ public class SRTSubtitleConverter implements SubtitleConverter { return new SubtitleImpl(text, start, end, null, null); }).toList(); final var text = subtitles.stream().map(Subtitle::content).collect(Collectors.joining(" ")); - return new SubtitleCollectionImpl(text, subtitles, translator.getLanguage(text)); + return new SubtitleCollectionImpl<>(text, subtitles, translator.getLanguage(text)); } catch (final Exception e) { throw new ParseException(e); } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java index 194b5f6..4b6d538 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java @@ -13,8 +13,8 @@ import static java.util.Objects.requireNonNull; /** * Implementation of {@link SubtitleCollection} */ -public record SubtitleCollectionImpl(String text, Collection subtitles, - Language language) implements SubtitleCollection { +public record SubtitleCollectionImpl(String text, Collection subtitles, + Language language) implements SubtitleCollection { public SubtitleCollectionImpl { Objects.requireNonNull(text); diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImpl.java index acb864b..5330c7d 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImpl.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImpl.java @@ -22,4 +22,8 @@ public record SubtitleImpl(String content, long start, long end, Font font, Boun throw new IllegalArgumentException("start must be <= end : " + start + " > " + end); } } + + public SubtitleImpl(final Subtitle subtitle) { + this(subtitle.content(), subtitle.start(), subtitle.end(), subtitle.font(), subtitle.bounds()); + } } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImporterExporterImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImporterExporterImpl.java new file mode 100644 index 0000000..b930f60 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImporterExporterImpl.java @@ -0,0 +1,141 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.Language; +import com.github.gtache.autosubtitle.archive.Archiver; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; +import com.github.gtache.autosubtitle.subtitle.SubtitleImporterExporter; +import com.github.gtache.autosubtitle.subtitle.converter.ParseException; +import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Implementation of {@link SubtitleImporterExporter} + */ +public class SubtitleImporterExporterImpl implements SubtitleImporterExporter { + + private static final Logger logger = LogManager.getLogger(SubtitleImporterExporterImpl.class); + private final Map archiverMap; + private final Map> converterMap; + + @Inject + SubtitleImporterExporterImpl(final Map archiverMap, final Map converterMap) { + this.archiverMap = Map.copyOf(archiverMap); + this.converterMap = converterMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public Map> importSubtitles(final Path file) throws IOException, ParseException { + final var fileName = file.getFileName().toString(); + final var extension = fileName.substring(fileName.lastIndexOf('.') + 1); + if (archiverMap.containsKey(extension)) { + return loadArchive(file); + } else { + final var loaded = loadSingleFile(file); + logger.info("Loaded {}", file); + return Map.of(loaded.language(), loaded); + } + } + + private SubtitleCollection loadSingleFile(final Path file) throws ParseException { + final var fileName = file.getFileName().toString(); + final var extension = fileName.substring(fileName.lastIndexOf('.') + 1); + final var parser = converterMap.get(extension); + if (parser == null) { + throw new ParseException("No converter found for " + file); + } else { + final var parsed = parser.parse(file); + return new SubtitleCollectionImpl<>(parsed.text(), parsed.subtitles().stream().map(SubtitleImpl::new).toList(), parsed.language()); + } + } + + private Map> loadArchive(final Path file) throws IOException, ParseException { + final var fileName = file.getFileName().toString(); + final var extension = fileName.substring(fileName.lastIndexOf('.') + 1); + final var archiver = archiverMap.get(extension); + final var tempDirectory = Files.createTempDirectory("autosubtitle"); + archiver.decompress(file, tempDirectory); + final var files = new ArrayList(); + try (final var stream = Files.list(tempDirectory)) { + stream.forEach(files::add); + } + final var map = new EnumMap>(Language.class); + for (final var path : files) { + final var loaded = loadSingleFile(path); + map.put(loaded.language(), loaded); + Files.deleteIfExists(path); + } + Files.deleteIfExists(tempDirectory); + logger.info("Loaded {}", file); + return map; + } + + @Override + public void exportSubtitles(final Collection> collections, final Path file) throws IOException { + final var fileName = file.getFileName().toString(); + final var extension = fileName.substring(fileName.lastIndexOf('.') + 1); + if (archiverMap.containsKey(extension)) { + saveArchive(file, collections); + } else if (collections.size() == 1) { + saveSingleFile(file, collections.iterator().next()); + } else { + throw new IllegalArgumentException("Cannot export multiple collections to a non-archive file : " + file); + } + } + + private void saveArchive(final Path file, final Iterable> collections) throws IOException { + final var fileName = file.getFileName().toString(); + final var extension = fileName.substring(fileName.lastIndexOf('.') + 1); + final var archiver = archiverMap.get(extension); + final var singleExporter = converterMap.getOrDefault("json", converterMap.values().iterator().next()); + final var tempDir = Files.createTempDirectory("autosubtitle"); + for (final var collection : collections) { + final var subtitleFile = tempDir.resolve(collection.language() + "." + singleExporter.formatName()); + saveSingleFile(subtitleFile, collection); + } + final var files = new ArrayList(); + try (final var stream = Files.list(tempDir)) { + stream.forEach(files::add); + } + archiver.compress(files, file); + for (final var path : files) { + Files.deleteIfExists(path); + } + Files.deleteIfExists(tempDir); + logger.info("Saved {}", file); + } + + private void saveSingleFile(final Path file, final SubtitleCollection collection) throws IOException { + final var fileName = file.getFileName().toString(); + final var extension = fileName.substring(fileName.lastIndexOf('.') + 1); + final var converter = converterMap.get(extension); + if (converter == null) { + throw new IOException("No converter found for " + file); + } else { + final var string = converter.format(collection); + Files.writeString(file, string); + logger.info("Saved {}", file); + } + } + + + @Override + public Collection supportedArchiveExtensions() { + return archiverMap.keySet(); + } + + @Override + public Collection supportedSingleFileExtensions() { + return converterMap.keySet(); + } +} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index d98ee39..2b88992 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -9,6 +9,7 @@ module com.github.gtache.autosubtitle.core { requires org.apache.logging.log4j; exports com.github.gtache.autosubtitle.impl; + exports com.github.gtache.autosubtitle.archive.impl; exports com.github.gtache.autosubtitle.process.impl; exports com.github.gtache.autosubtitle.setup.impl; exports com.github.gtache.autosubtitle.subtitle.impl; diff --git a/core/src/test/java/com/github/gtache/autosubtitle/archive/impl/TestZipDecompresser.java b/core/src/test/java/com/github/gtache/autosubtitle/archive/impl/TestZipDecompresser.java new file mode 100644 index 0000000..c89c8cf --- /dev/null +++ b/core/src/test/java/com/github/gtache/autosubtitle/archive/impl/TestZipDecompresser.java @@ -0,0 +1,56 @@ +package com.github.gtache.autosubtitle.archive.impl; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +class TestZipDecompresser { + + private final ZipDecompresser zipDecompresser; + + TestZipDecompresser() { + this.zipDecompresser = new ZipDecompresser(); + } + + @Test + void testIsPathSupported() { + assertTrue(zipDecompresser.isPathSupported(Path.of("test.zip"))); + assertFalse(zipDecompresser.isPathSupported(Path.of("test"))); + assertFalse(zipDecompresser.isPathSupported(Path.of("test.txt"))); + assertFalse(zipDecompresser.isPathSupported(Path.of("test.zip2"))); + } + + @Test + void testDecompress(@TempDir final Path tempDir) throws IOException { + final var file = tempDir.resolve("test.zip"); + try (final var in = getClass().getResourceAsStream("in.zip")) { + Files.copy(in, file); + } + zipDecompresser.decompress(file, tempDir); + final var inTxt = tempDir.resolve("in.txt"); + final var bin = tempDir.resolve("bin"); + final var binTxt = bin.resolve("bin.txt"); + final var lib = tempDir.resolve("lib"); + final var libTxt = lib.resolve("lib.txt"); + + assertTrue(Files.exists(inTxt)); + assertEquals("in", Files.readString(inTxt)); + assertTrue(Files.exists(bin)); + assertTrue(Files.exists(binTxt)); + assertEquals("bin", Files.readString(binTxt)); + assertTrue(Files.exists(lib)); + assertTrue(Files.exists(libTxt)); + assertEquals("lib", Files.readString(libTxt)); + } + + @Test + void testIllegal() { + assertThrows(IllegalArgumentException.class, () -> zipDecompresser.decompress(Paths.get("file.txt"), Paths.get("target"))); + } +} diff --git a/core/src/test/java/com/github/gtache/autosubtitle/process/impl/TestAbstractProcessRunner.java b/core/src/test/java/com/github/gtache/autosubtitle/process/impl/TestAbstractProcessRunner.java index 7e7b8de..f9a0405 100644 --- a/core/src/test/java/com/github/gtache/autosubtitle/process/impl/TestAbstractProcessRunner.java +++ b/core/src/test/java/com/github/gtache/autosubtitle/process/impl/TestAbstractProcessRunner.java @@ -22,7 +22,7 @@ class TestAbstractProcessRunner { @Test void testRun() throws IOException { final var expected = new ProcessResultImpl(0, List.of("1", "2", "3")); - final var actual = dummyProcessRunner.run(ARGS); + final var actual = dummyProcessRunner.run(ARGS, Duration.ofSeconds(5)); assertEquals(expected, actual); } @@ -50,7 +50,7 @@ class TestAbstractProcessRunner { @Test void testRunListen() throws IOException { - final var result = dummyProcessRunner.runListen(ARGS); + final var result = dummyProcessRunner.runListen(ARGS, Duration.ofSeconds(5)); assertEquals(0, result.exitCode()); assertEquals(List.of("1", "2", "3"), result.output()); } diff --git a/core/src/test/java/com/github/gtache/autosubtitle/subtitle/converter/impl/TestSRTSubtitleConverter.java b/core/src/test/java/com/github/gtache/autosubtitle/subtitle/converter/impl/TestSRTSubtitleConverter.java index 13d827a..46e6be1 100644 --- a/core/src/test/java/com/github/gtache/autosubtitle/subtitle/converter/impl/TestSRTSubtitleConverter.java +++ b/core/src/test/java/com/github/gtache/autosubtitle/subtitle/converter/impl/TestSRTSubtitleConverter.java @@ -50,7 +50,7 @@ class TestSRTSubtitleConverter { final var end2 = 12L * 60 * 60 * 1000 + 23 * 60 * 1000 + 34 * 1000 + 457; final var subtitle1 = new SubtitleImpl("test5 test6\ntest7 test8", start1, end1, null, null); final var subtitle2 = new SubtitleImpl("test1 test2\ntest3 test4", start2, end2, null, null); - final var subtitles = new SubtitleCollectionImpl(subtitle1.content() + " " + subtitle2.content(), Arrays.asList(subtitle1, subtitle2), language); + final var subtitles = new SubtitleCollectionImpl<>(subtitle1.content() + " " + subtitle2.content(), Arrays.asList(subtitle1, subtitle2), language); final var converter = new SRTSubtitleConverter(translator); assertEquals(subtitles, converter.parse(in)); assertEquals(in, converter.format(subtitles)); diff --git a/core/src/test/java/com/github/gtache/autosubtitle/subtitle/extractor/impl/TestAbstractSubtitleExtractor.java b/core/src/test/java/com/github/gtache/autosubtitle/subtitle/extractor/impl/TestAbstractSubtitleExtractor.java index b7811a6..2e80774 100644 --- a/core/src/test/java/com/github/gtache/autosubtitle/subtitle/extractor/impl/TestAbstractSubtitleExtractor.java +++ b/core/src/test/java/com/github/gtache/autosubtitle/subtitle/extractor/impl/TestAbstractSubtitleExtractor.java @@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.subtitle.extractor.impl; import com.github.gtache.autosubtitle.Audio; import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.extractor.ExtractEvent; import com.github.gtache.autosubtitle.subtitle.extractor.ExtractException; @@ -56,12 +57,12 @@ class TestAbstractSubtitleExtractor { private static final class DummySubtitleExtractor extends AbstractSubtitleExtractor { @Override - public SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException { + public SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException { throw new UnsupportedOperationException(); } @Override - public SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException { + public SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException { throw new UnsupportedOperationException(); } } diff --git a/core/src/test/resources/com/github/gtache/autosubtitle/archive/impl/in.txt b/core/src/test/resources/com/github/gtache/autosubtitle/archive/impl/in.txt new file mode 100644 index 0000000..f087d89 --- /dev/null +++ b/core/src/test/resources/com/github/gtache/autosubtitle/archive/impl/in.txt @@ -0,0 +1 @@ +in \ No newline at end of file diff --git a/core/src/test/resources/com/github/gtache/autosubtitle/archive/impl/in.zip b/core/src/test/resources/com/github/gtache/autosubtitle/archive/impl/in.zip new file mode 100644 index 0000000..9261792 Binary files /dev/null and b/core/src/test/resources/com/github/gtache/autosubtitle/archive/impl/in.zip differ diff --git a/core/src/test/resources/log4j2-test.xml b/core/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..98cfd73 --- /dev/null +++ b/core/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/deepl/src/main/java/com/github/gtache/autosubtitle/deepl/DeepLTranslator.java b/deepl/src/main/java/com/github/gtache/autosubtitle/deepl/DeepLTranslator.java index 3fa8cb4..b5b302b 100644 --- a/deepl/src/main/java/com/github/gtache/autosubtitle/deepl/DeepLTranslator.java +++ b/deepl/src/main/java/com/github/gtache/autosubtitle/deepl/DeepLTranslator.java @@ -10,7 +10,7 @@ import javax.inject.Inject; /** * DeepL implementation of {@link Translator} */ -public class DeepLTranslator implements Translator { +public class DeepLTranslator implements Translator { @Inject DeepLTranslator() { @@ -32,7 +32,7 @@ public class DeepLTranslator implements Translator { } @Override - public SubtitleCollection translate(final SubtitleCollection collection, final Language to) { + public SubtitleCollection translate(final SubtitleCollection collection, final Language to) { return null; } } diff --git a/ffmpeg/pom.xml b/ffmpeg/pom.xml index 1ae6047..2f2b087 100644 --- a/ffmpeg/pom.xml +++ b/ffmpeg/pom.xml @@ -24,6 +24,11 @@ org.tukaani xz + + org.apache.logging.log4j + log4j-core + test + \ No newline at end of file diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/TarDecompresser.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/archive/ffmpeg/TarArchiver.java similarity index 73% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/TarDecompresser.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/archive/ffmpeg/TarArchiver.java index a86568d..6e9bc37 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/TarDecompresser.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/archive/ffmpeg/TarArchiver.java @@ -1,17 +1,31 @@ -package com.github.gtache.autosubtitle.setup.ffmpeg; +package com.github.gtache.autosubtitle.archive.ffmpeg; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import com.github.gtache.autosubtitle.archive.Archiver; +import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import javax.inject.Inject; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; /** - * Tar implementation of {@link Decompresser} + * Tar implementation of {@link Archiver} */ -public class TarDecompresser implements Decompresser { +public class TarArchiver implements Archiver { + + @Inject + TarArchiver() { + + } + + @Override + public void compress(final List files, final Path destination) throws IOException { + throw new UnsupportedOperationException("Not implemented yet"); + } + @Override public void decompress(final Path archive, final Path destination) throws IOException { if (!isPathSupported(archive)) { @@ -38,7 +52,7 @@ public class TarDecompresser implements Decompresser { } } - private static Path newFile(final Path destinationDir, final TarArchiveEntry entry) throws IOException { + private static Path newFile(final Path destinationDir, final ArchiveEntry entry) throws IOException { final var destPath = destinationDir.resolve(entry.getName()); final var destDirPath = destinationDir.toAbsolutePath().toString(); @@ -51,7 +65,7 @@ public class TarDecompresser implements Decompresser { } @Override - public boolean isPathSupported(final Path path) { - return path.getFileName().toString().endsWith(".tar"); + public String archiveExtension() { + return "tar"; } } diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/archive/ffmpeg/XZArchiver.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/archive/ffmpeg/XZArchiver.java new file mode 100644 index 0000000..3c13593 --- /dev/null +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/archive/ffmpeg/XZArchiver.java @@ -0,0 +1,45 @@ +package com.github.gtache.autosubtitle.archive.ffmpeg; + +import com.github.gtache.autosubtitle.archive.Archiver; +import org.tukaani.xz.XZInputStream; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * XZ implementation of {@link Archiver} + */ +public class XZArchiver implements Archiver { + + @Inject + XZArchiver() { + + } + + @Override + public void compress(final List files, final Path destination) throws IOException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void decompress(final Path archive, final Path destination) throws IOException { + if (!isPathSupported(archive)) { + throw new IllegalArgumentException("Unsupported path : " + archive); + } + final var archiveName = archive.getFileName().toString(); + Files.createDirectories(destination); + final var destinationFile = destination.resolve(archiveName.substring(0, archiveName.lastIndexOf(".xz"))); + try (final var xzIn = new XZInputStream(Files.newInputStream(archive)); + final var out = Files.newOutputStream(destinationFile)) { + xzIn.transferTo(out); + } + } + + @Override + public String archiveExtension() { + return "xz"; + } +} diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java index afebd92..4180ac7 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java @@ -8,17 +8,17 @@ import com.github.gtache.autosubtitle.impl.AudioInfoImpl; import com.github.gtache.autosubtitle.impl.FileAudioImpl; import com.github.gtache.autosubtitle.impl.FileVideoImpl; import com.github.gtache.autosubtitle.impl.VideoInfoImpl; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath; +import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegBundledPath; +import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegSystemPath; import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; import javax.inject.Inject; -import javax.inject.Singleton; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -32,30 +32,29 @@ import static java.util.Objects.requireNonNull; /** * FFmpeg implementation of {@link VideoConverter} */ -@Singleton public class FFmpegVideoConverter extends AbstractProcessRunner implements VideoConverter { private static final String TEMP_FILE_PREFIX = "autosubtitle"; private final Path bundledPath; private final Path systemPath; - private final SubtitleConverter subtitleConverter; + private final SubtitleConverter subtitleConverter; @Inject FFmpegVideoConverter(@FFmpegBundledPath final Path bundledPath, @FFmpegSystemPath final Path systemPath, final Map subtitleConverters) { this.bundledPath = requireNonNull(bundledPath); this.systemPath = requireNonNull(systemPath); - this.subtitleConverter = requireNonNull(subtitleConverters.get("srt")); + this.subtitleConverter = subtitleConverters.get("srt"); } @Override - public Video addSoftSubtitles(final Video video, final Collection subtitles) throws IOException { - final var out = getTempFile("mkv"); //Soft ass subtitles are only supported by mkv apparently + public Video addSoftSubtitles(final Video video, final Collection> subtitles) throws IOException { + final var out = getTempFile(video.info().videoFormat()); addSoftSubtitles(video, subtitles, out); - return new FileVideoImpl(out, new VideoInfoImpl("mkv", video.info().width(), video.info().height(), video.info().duration())); + return new FileVideoImpl(out, new VideoInfoImpl(video.info().videoFormat(), video.info().width(), video.info().height(), video.info().duration())); } @Override - public void addSoftSubtitles(final Video video, final Collection subtitles, final Path path) throws IOException { + public void addSoftSubtitles(final Video video, final Collection> subtitles, final Path path) throws IOException { final var videoPath = getPath(video); final var collectionMap = dumpCollections(subtitles); final var args = new ArrayList(); @@ -96,18 +95,18 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video args.add("language=" + c.language().iso3()); }); args.add(path.toString()); - runListen(args); + runListen(args, Duration.ofHours(1)); } @Override - public Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException { + public Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException { final var out = getTempFile(video.info().videoFormat()); addHardSubtitles(video, subtitles, out); return new FileVideoImpl(out, video.info()); } @Override - public void addHardSubtitles(final Video video, final SubtitleCollection subtitles, final Path path) throws IOException { + public void addHardSubtitles(final Video video, final SubtitleCollection subtitles, final Path path) throws IOException { final var videoPath = getPath(video); final var subtitlesPath = dumpSubtitles(subtitles); final var escapedPath = escapeVF(subtitlesPath.toString()); @@ -120,7 +119,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video subtitleArg, path.toString() ); - runListen(args); + runListen(args, Duration.ofHours(1)); } private static String escapeVF(final String path) { @@ -145,7 +144,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video "0:v", dumpVideoPath.toString() ); - runListen(args); + runListen(args, Duration.ofHours(1)); Files.deleteIfExists(dumpVideoPath); return new FileAudioImpl(audioPath, new AudioInfoImpl("wav", video.info().duration())); } @@ -166,15 +165,15 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video return path; } - private SequencedMap dumpCollections(final Collection collections) throws IOException { - final var ret = LinkedHashMap.newLinkedHashMap(collections.size()); + private > SequencedMap dumpCollections(final Collection collections) throws IOException { + final var ret = LinkedHashMap.newLinkedHashMap(collections.size()); for (final var subtitles : collections) { ret.put(subtitles, dumpSubtitles(subtitles)); } return ret; } - private Path dumpSubtitles(final SubtitleCollection subtitles) throws IOException { + private Path dumpSubtitles(final SubtitleCollection subtitles) throws IOException { final var path = getTempFile("srt"); Files.writeString(path, subtitleConverter.format(subtitles)); return path; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFprobeVideoLoader.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFprobeVideoLoader.java index 272d55b..97d965e 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFprobeVideoLoader.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFprobeVideoLoader.java @@ -4,22 +4,22 @@ import com.github.gtache.autosubtitle.Video; import com.github.gtache.autosubtitle.VideoLoader; import com.github.gtache.autosubtitle.impl.FileVideoImpl; import com.github.gtache.autosubtitle.impl.VideoInfoImpl; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeBundledPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath; +import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFprobeBundledPath; +import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFprobeSystemPath; import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner; import javax.inject.Inject; -import javax.inject.Singleton; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; +import java.util.List; import static java.util.Objects.requireNonNull; /** * FFprobe implementation of {@link VideoLoader} */ -@Singleton public class FFprobeVideoLoader extends AbstractProcessRunner implements VideoLoader { private final Path bundledPath; @@ -33,7 +33,7 @@ public class FFprobeVideoLoader extends AbstractProcessRunner implements VideoLo @Override public Video loadVideo(final Path path) throws IOException { - final var result = run(getFFprobePath(), "-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height,duration", "-of", "csv=p=0", path.toString()); + final var result = run(List.of(getFFprobePath(), "-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height,duration", "-of", "csv=p=0", path.toString()), Duration.ofSeconds(5)); final var resolution = result.output().getLast(); final var split = resolution.split(","); final var width = Integer.parseInt(split[0]); diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFBundledRoot.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFBundledRoot.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFBundledRoot.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFBundledRoot.java index b96beb8..104d206 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFBundledRoot.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFBundledRoot.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFProbeInstallerPath.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFProbeInstallerPath.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFProbeInstallerPath.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFProbeInstallerPath.java index b9a6142..83d0969 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFProbeInstallerPath.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFProbeInstallerPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegBundledPath.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegBundledPath.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegBundledPath.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegBundledPath.java index 3d16cf7..4b7ef39 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegBundledPath.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegBundledPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegInstallerPath.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegInstallerPath.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegInstallerPath.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegInstallerPath.java index 3e9d794..bd86030 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegInstallerPath.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegInstallerPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java index c617df7..acef204 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java @@ -1,24 +1,17 @@ package com.github.gtache.autosubtitle.modules.setup.ffmpeg; +import com.github.gtache.autosubtitle.archive.Archiver; +import com.github.gtache.autosubtitle.archive.ffmpeg.TarArchiver; +import com.github.gtache.autosubtitle.archive.ffmpeg.XZArchiver; import com.github.gtache.autosubtitle.impl.Architecture; import com.github.gtache.autosubtitle.impl.OS; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFBundledRoot; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFProbeInstallerPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegInstallerPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeBundledPath; -import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath; import com.github.gtache.autosubtitle.modules.impl.ExecutableExtension; +import com.github.gtache.autosubtitle.modules.setup.impl.CacheRoot; +import com.github.gtache.autosubtitle.modules.setup.impl.ToolsRoot; import com.github.gtache.autosubtitle.modules.setup.impl.VideoConverterSetup; import com.github.gtache.autosubtitle.setup.SetupManager; -import com.github.gtache.autosubtitle.setup.ffmpeg.Decompresser; import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupConfiguration; import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager; -import com.github.gtache.autosubtitle.setup.ffmpeg.TarDecompresser; -import com.github.gtache.autosubtitle.setup.ffmpeg.XZDecompresser; -import com.github.gtache.autosubtitle.setup.ffmpeg.ZipDecompresser; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -41,20 +34,16 @@ public abstract class FFmpegSetupModule { } - @Binds - @StringKey("zip") - @IntoMap - abstract Decompresser bindsZipDecompresser(final ZipDecompresser decompresser); @Binds @StringKey("tar") @IntoMap - abstract Decompresser bindsTarDecompresser(final TarDecompresser decompresser); + abstract Archiver bindsTarDecompresser(final TarArchiver decompresser); @Binds @StringKey("xz") @IntoMap - abstract Decompresser bindsXzDecompresser(final XZDecompresser decompresser); + abstract Archiver bindsXZDecompresser(final XZArchiver decompresser); @Binds @VideoConverterSetup @@ -69,19 +58,19 @@ public abstract class FFmpegSetupModule { @Provides @FFmpegInstallerPath - static Path providesFFmpegInstallerPath(@FFBundledRoot final Path root, final OS os) { - return root.resolve("cache").resolve("ffmpeg-installer" + getInstallerExtension(os)); + static Path providesFFmpegInstallerPath(@CacheRoot final Path root, final OS os) { + return root.resolve(FFMPEG + "-installer" + getInstallerExtension(os)); } @Provides @FFProbeInstallerPath - static Path providesFFProbeInstallerPath(@FFBundledRoot final Path root, final OS os) { - return root.resolve("cache").resolve("ffprobe-installer" + getInstallerExtension(os)); + static Path providesFFProbeInstallerPath(@CacheRoot final Path root, final OS os) { + return root.resolve(FFPROBE + "-installer" + getInstallerExtension(os)); } private static String getInstallerExtension(final OS os) { if (os == OS.LINUX) { - return ".tar.gz"; + return ".tar.xz"; } else { return ".zip"; } @@ -89,8 +78,8 @@ public abstract class FFmpegSetupModule { @Provides @FFBundledRoot - static Path providesFFBundledRoot() { - return Paths.get("tools", FFMPEG); + static Path providesFFBundledRoot(@ToolsRoot final Path root) { + return root.resolve(FFMPEG); } @Provides diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegSystemPath.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSystemPath.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegSystemPath.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSystemPath.java index 6b58fa3..c393a77 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegSystemPath.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSystemPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegVersion.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegVersion.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegVersion.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegVersion.java index a883339..c798f05 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegVersion.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegVersion.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFprobeBundledPath.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFprobeBundledPath.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFprobeBundledPath.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFprobeBundledPath.java index 9869e9e..df69cf1 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFprobeBundledPath.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFprobeBundledPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFprobeSystemPath.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFprobeSystemPath.java similarity index 86% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFprobeSystemPath.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFprobeSystemPath.java index b1ec1a6..f63e040 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFprobeSystemPath.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFprobeSystemPath.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/Decompresser.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/Decompresser.java deleted file mode 100644 index d93f9db..0000000 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/Decompresser.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.gtache.autosubtitle.setup.ffmpeg; - -import java.io.IOException; -import java.nio.file.Path; - -/** - * Unzips files - */ -public interface Decompresser { - - /** - * Unzips an archive to the given destination - * - * @param archive The archive - * @param destination The destination folder - * @throws IOException if an error occurs - */ - void decompress(final Path archive, final Path destination) throws IOException; - - /** - * Checks whether the given file is supported by the decompresser - * - * @param path The file path - * @return True if the file is supported - */ - boolean isPathSupported(final Path path); -} diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java index 81a6af2..9ec7533 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java @@ -1,5 +1,6 @@ package com.github.gtache.autosubtitle.setup.ffmpeg; +import com.github.gtache.autosubtitle.archive.Archiver; import com.github.gtache.autosubtitle.impl.Architecture; import com.github.gtache.autosubtitle.setup.SetupException; import com.github.gtache.autosubtitle.setup.SetupManager; @@ -13,6 +14,8 @@ import java.io.IOException; import java.net.http.HttpClient; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; +import java.util.List; import java.util.Map; import static com.github.gtache.autosubtitle.impl.Architecture.ARMEL; @@ -26,14 +29,14 @@ import static java.util.Objects.requireNonNull; public class FFmpegSetupManager extends AbstractSetupManager { private static final Logger logger = LogManager.getLogger(FFmpegSetupManager.class); private final FFmpegSetupConfiguration configuration; - private final Map decompressers; + private final Map archivers; @Inject - FFmpegSetupManager(final FFmpegSetupConfiguration configuration, final Map decompressers, + FFmpegSetupManager(final FFmpegSetupConfiguration configuration, final Map archivers, final HttpClient httpClient) { super(httpClient); this.configuration = requireNonNull(configuration); - this.decompressers = Map.copyOf(decompressers); + this.archivers = Map.copyOf(archivers); } @Override @@ -132,7 +135,7 @@ public class FFmpegSetupManager extends AbstractSetupManager { try { final var filename = from.getFileName().toString(); final var extension = filename.substring(filename.lastIndexOf('.') + 1); - decompressers.get(extension).decompress(from, to); + archivers.get(extension).decompress(from, to); } catch (final IOException e) { throw new SetupException(e); } @@ -188,7 +191,7 @@ public class FFmpegSetupManager extends AbstractSetupManager { } private boolean checkSystemFFmpeg() throws IOException { - final var result = run(configuration.systemFFmpegPath().toString(), "-version"); + final var result = run(List.of(configuration.systemFFmpegPath().toString(), "-version"), Duration.ofSeconds(5)); return result.exitCode() == 0; } diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/XZDecompresser.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/XZDecompresser.java deleted file mode 100644 index 81b4e77..0000000 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/XZDecompresser.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.gtache.autosubtitle.setup.ffmpeg; - -import org.tukaani.xz.XZInputStream; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * XZ implementation of {@link Decompresser} - */ -public class XZDecompresser implements Decompresser { - @Override - public void decompress(final Path archive, final Path destination) throws IOException { - if (!isPathSupported(archive)) { - throw new IllegalArgumentException("Unsupported path : " + archive); - } - try (final var xzIn = new XZInputStream(Files.newInputStream(archive)); - final var out = Files.newOutputStream(destination)) { - xzIn.transferTo(out); - } - } - - @Override - public boolean isPathSupported(final Path path) { - return path.getFileName().toString().endsWith(".xz"); - } -} diff --git a/ffmpeg/src/main/java/module-info.java b/ffmpeg/src/main/java/module-info.java index 59d58fa..4605994 100644 --- a/ffmpeg/src/main/java/module-info.java +++ b/ffmpeg/src/main/java/module-info.java @@ -15,4 +15,5 @@ module com.github.gtache.autosubtitle.ffmpeg { exports com.github.gtache.autosubtitle.modules.ffmpeg; exports com.github.gtache.autosubtitle.modules.setup.ffmpeg; + exports com.github.gtache.autosubtitle.archive.ffmpeg; } \ No newline at end of file diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/archive/ffmpeg/TestTarArchiver.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/archive/ffmpeg/TestTarArchiver.java new file mode 100644 index 0000000..0b1e818 --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/archive/ffmpeg/TestTarArchiver.java @@ -0,0 +1,56 @@ +package com.github.gtache.autosubtitle.archive.ffmpeg; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +class TestTarArchiver { + + private final TarArchiver tarArchiver; + + TestTarArchiver() { + this.tarArchiver = new TarArchiver(); + } + + @Test + void testIsPathSupported() { + assertTrue(tarArchiver.isPathSupported(Path.of("test.tar"))); + assertFalse(tarArchiver.isPathSupported(Path.of("test"))); + assertFalse(tarArchiver.isPathSupported(Path.of("test.txt"))); + assertFalse(tarArchiver.isPathSupported(Path.of("test.tar2"))); + } + + @Test + void testDecompress(@TempDir final Path tempDir) throws IOException { + final var file = tempDir.resolve("test.tar"); + try (final var in = getClass().getResourceAsStream("in.tar")) { + Files.copy(in, file); + } + tarArchiver.decompress(file, tempDir); + final var inTxt = tempDir.resolve("in.txt"); + final var bin = tempDir.resolve("bin"); + final var binTxt = bin.resolve("bin.txt"); + final var lib = tempDir.resolve("lib"); + final var libTxt = lib.resolve("lib.txt"); + + assertTrue(Files.exists(inTxt)); + assertEquals("in", Files.readString(inTxt)); + assertTrue(Files.exists(bin)); + assertTrue(Files.exists(binTxt)); + assertEquals("bin", Files.readString(binTxt)); + assertTrue(Files.exists(lib)); + assertTrue(Files.exists(libTxt)); + assertEquals("lib", Files.readString(libTxt)); + } + + @Test + void testIllegal() { + assertThrows(IllegalArgumentException.class, () -> tarArchiver.decompress(Paths.get("file.txt"), Paths.get("target"))); + } +} diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/archive/ffmpeg/TestXZArchiver.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/archive/ffmpeg/TestXZArchiver.java new file mode 100644 index 0000000..14ba0f6 --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/archive/ffmpeg/TestXZArchiver.java @@ -0,0 +1,46 @@ +package com.github.gtache.autosubtitle.archive.ffmpeg; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +class TestXZArchiver { + + private final XZArchiver xzArchiver; + + TestXZArchiver() { + this.xzArchiver = new XZArchiver(); + } + + @Test + void testIsPathSupported() { + assertTrue(xzArchiver.isPathSupported(Path.of("test.xz"))); + assertFalse(xzArchiver.isPathSupported(Path.of("test"))); + assertFalse(xzArchiver.isPathSupported(Path.of("test.txt"))); + assertFalse(xzArchiver.isPathSupported(Path.of("test.xz2"))); + } + + @Test + void testDecompress(@TempDir final Path tempDir) throws IOException { + final var file = tempDir.resolve("in.txt.xz"); + try (final var in = getClass().getResourceAsStream("in.txt.xz")) { + Files.copy(in, file); + } + xzArchiver.decompress(file, tempDir); + final var inTxt = tempDir.resolve("in.txt"); + + assertTrue(Files.isRegularFile(inTxt)); + assertEquals("in", Files.readString(inTxt)); + } + + @Test + void testIllegal() { + assertThrows(IllegalArgumentException.class, () -> xzArchiver.decompress(Paths.get("file.txt"), Paths.get("target"))); + } +} diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/ffmpeg/TestFFmpegVideoConverter.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/ffmpeg/TestFFmpegVideoConverter.java new file mode 100644 index 0000000..7ce2845 --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/ffmpeg/TestFFmpegVideoConverter.java @@ -0,0 +1,57 @@ +package com.github.gtache.autosubtitle.ffmpeg; + +import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.VideoInfo; +import com.github.gtache.autosubtitle.impl.OS; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; +import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.Objects; + +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TestFFmpegVideoConverter { + + private final FFmpegVideoConverter converter; + private final SubtitleConverter subtitleConverter; + private final Video video; + private final VideoInfo videoInfo; + private final Path tmpFile; + private final Path outputPath; + private final SubtitleCollection collection; + + TestFFmpegVideoConverter(@Mock final SubtitleConverter subtitleConverter, @Mock final Video video, + @Mock final VideoInfo videoInfo, @Mock final SubtitleCollection collection) throws IOException { + final var output = (OS.getOS() == OS.WINDOWS ? System.getProperty("java.io.tmpdir") : "/tmp"); + final var resource = OS.getOS() == OS.WINDOWS ? "fake-ffmpeg.exe" : "fake-ffmpeg.sh"; + this.video = Objects.requireNonNull(video); + this.videoInfo = Objects.requireNonNull(videoInfo); + when(video.info()).thenReturn(videoInfo); + this.tmpFile = Files.createTempFile("fake-ffmpeg", resource.substring(resource.lastIndexOf('.'))); + try (final var in = getClass().getResourceAsStream(resource)) { + Files.copy(in, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + this.outputPath = Path.of(output, "test-ffmpeg-output.txt"); + this.subtitleConverter = Objects.requireNonNull(subtitleConverter); + this.converter = new FFmpegVideoConverter(tmpFile, tmpFile, Map.of("srt", subtitleConverter)); + this.collection = Objects.requireNonNull(collection); + } + + @AfterEach + void afterEach() throws IOException { + Files.deleteIfExists(tmpFile); + Files.deleteIfExists(outputPath); + } + + //TODO tests +} diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/ffmpeg/TestFFmpegVideoLoader.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/ffmpeg/TestFFmpegVideoLoader.java new file mode 100644 index 0000000..faef35e --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/ffmpeg/TestFFmpegVideoLoader.java @@ -0,0 +1,51 @@ +package com.github.gtache.autosubtitle.ffmpeg; + +import com.github.gtache.autosubtitle.impl.FileVideoImpl; +import com.github.gtache.autosubtitle.impl.OS; +import com.github.gtache.autosubtitle.impl.VideoInfoImpl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestFFmpegVideoLoader { + + private final FFprobeVideoLoader loader; + private final Path tmpFile; + private final Path outputPath; + + TestFFmpegVideoLoader() throws IOException { + final var output = (OS.getOS() == OS.WINDOWS ? System.getProperty("java.io.tmpdir") : "/tmp"); + final var resource = OS.getOS() == OS.WINDOWS ? "fake-ffprobe.exe" : "fake-ffprobe.sh"; + this.tmpFile = Files.createTempFile("fake-ffprobe", resource.substring(resource.lastIndexOf('.'))); + try (final var in = getClass().getResourceAsStream(resource)) { + Files.copy(in, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + this.outputPath = Path.of(output, "test-ffprobe-output.txt"); + this.loader = new FFprobeVideoLoader(tmpFile, tmpFile); + } + + @AfterEach + void afterEach() throws IOException { + Files.deleteIfExists(tmpFile); + Files.deleteIfExists(outputPath); + } + + @Test + @Disabled("Doesn't work") + void testLoadVideo() throws IOException { + final var in = Paths.get("in.mp4"); + final var expectedInfo = new VideoInfoImpl("mp4", 1920, 1080, 25000L); + final var expected = new FileVideoImpl(in, expectedInfo); + final var result = loader.loadVideo(in); + assertEquals(expected, result); + assertEquals("-v error -select_streams v -show_entries stream=width,height,duration -of csv=p=0 in.mp4", Files.readString(outputPath)); + } +} diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/TestFFmpegSetupModule.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/TestFFmpegSetupModule.java new file mode 100644 index 0000000..a685128 --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/TestFFmpegSetupModule.java @@ -0,0 +1,84 @@ +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; + +import com.github.gtache.autosubtitle.impl.Architecture; +import com.github.gtache.autosubtitle.impl.OS; +import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupConfiguration; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import static com.github.gtache.autosubtitle.impl.OS.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +class TestFFmpegSetupModule { + + private static final String FFMPEG = "ffmpeg"; + private static final String FFPROBE = "ffprobe"; + + private final Path root; + private final String extension; + + TestFFmpegSetupModule() { + this.root = Paths.get("root"); + this.extension = "ext"; + } + + @Test + void testFFmpegSetupConfiguration() { + final var bundledPath = mock(Path.class); + final var systemPath = mock(Path.class); + final var ffmpegInstallerPath = mock(Path.class); + final var ffprobeInstallerPath = mock(Path.class); + final var os = mock(OS.class); + final var architecture = mock(Architecture.class); + final var expected = new FFmpegSetupConfiguration(root, bundledPath, systemPath, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture); + assertEquals(expected, FFmpegSetupModule.providesFFmpegSetupConfiguration(root, bundledPath, systemPath, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture)); + } + + @Test + void testProvidesFFmpegInstallerPath() { + assertEquals(root.resolve(FFMPEG + "-installer.tar.xz"), FFmpegSetupModule.providesFFmpegInstallerPath(root, LINUX)); + assertEquals(root.resolve(FFMPEG + "-installer.zip"), FFmpegSetupModule.providesFFmpegInstallerPath(root, WINDOWS)); + assertEquals(root.resolve(FFMPEG + "-installer.zip"), FFmpegSetupModule.providesFFmpegInstallerPath(root, MAC)); + } + + @Test + void testProvidesFFProbeInstallerPath() { + assertEquals(root.resolve(FFPROBE + "-installer.tar.xz"), FFmpegSetupModule.providesFFProbeInstallerPath(root, LINUX)); + assertEquals(root.resolve(FFPROBE + "-installer.zip"), FFmpegSetupModule.providesFFProbeInstallerPath(root, WINDOWS)); + assertEquals(root.resolve(FFPROBE + "-installer.zip"), FFmpegSetupModule.providesFFProbeInstallerPath(root, MAC)); + } + + @Test + void testProvidesBundledRoot() { + assertEquals(root.resolve(FFMPEG), FFmpegSetupModule.providesFFBundledRoot(root)); + } + + @Test + void testFFprobeBundledPath() { + assertEquals(root.resolve(FFPROBE + extension), FFmpegSetupModule.providesFFProbeBundledPath(root, extension)); + } + + @Test + void testFFprobeSystemPath() { + assertEquals(Paths.get(FFPROBE + extension), FFmpegSetupModule.providesFFProbeSystemPath(extension)); + } + + + @Test + void testFFmpegBundledPath() { + assertEquals(root.resolve(FFMPEG + extension), FFmpegSetupModule.providesFFmpegBundledPath(root, extension)); + } + + @Test + void testFFmpegSystemPath() { + assertEquals(Paths.get(FFMPEG + extension), FFmpegSetupModule.providesFFmpegSystemPath(extension)); + } + + @Test + void testVersion() { + assertEquals("7.0.1", FFmpegSetupModule.providesFFmpegVersion()); + } +} diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/setup/ffmpeg/TestFFmpegSetupConfiguration.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/setup/ffmpeg/TestFFmpegSetupConfiguration.java new file mode 100644 index 0000000..6652de6 --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/setup/ffmpeg/TestFFmpegSetupConfiguration.java @@ -0,0 +1,62 @@ +package com.github.gtache.autosubtitle.setup.ffmpeg; + +import com.github.gtache.autosubtitle.impl.Architecture; +import com.github.gtache.autosubtitle.impl.OS; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.nio.file.Path; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +class TestFFmpegSetupConfiguration { + + private final Path root; + private final Path bundledFFmpegPath; + private final Path systemFFmpegPath; + private final Path ffmpegInstallerPath; + private final Path ffprobeInstallerPath; + private final OS os; + private final Architecture architecture; + + TestFFmpegSetupConfiguration(@Mock final Path root, @Mock final Path bundledFFmpegPath, + @Mock final Path systemFFmpegPath, @Mock final Path ffmpegInstallerPath, + @Mock final Path ffprobeInstallerPath, @Mock final OS os, + @Mock final Architecture architecture) { + this.root = requireNonNull(root); + this.bundledFFmpegPath = requireNonNull(bundledFFmpegPath); + this.systemFFmpegPath = requireNonNull(systemFFmpegPath); + this.ffmpegInstallerPath = requireNonNull(ffmpegInstallerPath); + this.ffprobeInstallerPath = requireNonNull(ffprobeInstallerPath); + this.os = requireNonNull(os); + this.architecture = requireNonNull(architecture); + } + + @Test + void testGetters() { + final var config = new FFmpegSetupConfiguration(root, bundledFFmpegPath, systemFFmpegPath, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture); + assertEquals(root, config.root()); + assertEquals(bundledFFmpegPath, config.bundledFFmpegPath()); + assertEquals(systemFFmpegPath, config.systemFFmpegPath()); + assertEquals(ffmpegInstallerPath, config.ffmpegInstallerPath()); + assertEquals(ffprobeInstallerPath, config.ffprobeInstallerPath()); + assertEquals(os, config.os()); + assertEquals(architecture, config.architecture()); + } + + @Test + void testIllegal() { + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(null, bundledFFmpegPath, systemFFmpegPath, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture)); + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(root, null, systemFFmpegPath, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture)); + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(root, bundledFFmpegPath, null, ffmpegInstallerPath, ffprobeInstallerPath, os, architecture)); + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(root, bundledFFmpegPath, systemFFmpegPath, null, ffprobeInstallerPath, os, architecture)); + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(root, bundledFFmpegPath, systemFFmpegPath, ffmpegInstallerPath, null, os, architecture)); + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(root, bundledFFmpegPath, systemFFmpegPath, ffmpegInstallerPath, ffprobeInstallerPath, null, architecture)); + assertThrows(NullPointerException.class, () -> new FFmpegSetupConfiguration(root, bundledFFmpegPath, systemFFmpegPath, ffmpegInstallerPath, ffprobeInstallerPath, os, null)); + } +} diff --git a/ffmpeg/src/test/java/com/github/gtache/autosubtitle/setup/ffmpeg/TestFFmpegSetupManager.java b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/setup/ffmpeg/TestFFmpegSetupManager.java new file mode 100644 index 0000000..e0cf5bc --- /dev/null +++ b/ffmpeg/src/test/java/com/github/gtache/autosubtitle/setup/ffmpeg/TestFFmpegSetupManager.java @@ -0,0 +1,4 @@ +package com.github.gtache.autosubtitle.setup.ffmpeg; + +class TestFFmpegSetupManager { +} diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/bin.txt b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/bin.txt new file mode 100644 index 0000000..c5e82d7 --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/bin.txt @@ -0,0 +1 @@ +bin \ No newline at end of file diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.tar b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.tar new file mode 100644 index 0000000..a565fa1 Binary files /dev/null and b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.tar differ diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.txt b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.txt new file mode 100644 index 0000000..f087d89 --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.txt @@ -0,0 +1 @@ +in \ No newline at end of file diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.txt.xz b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.txt.xz new file mode 100644 index 0000000..d6019bd Binary files /dev/null and b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/in.txt.xz differ diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/lib.txt b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/lib.txt new file mode 100644 index 0000000..7951405 --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/archive/ffmpeg/lib.txt @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.exe b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.exe new file mode 100644 index 0000000..2bc1a21 Binary files /dev/null and b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.exe differ diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.ps1 b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.ps1 new file mode 100644 index 0000000..cfd8317 --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.ps1 @@ -0,0 +1,5 @@ +$TempDir = [System.IO.Path]::GetTempPath() +$Output = "$TempDir\test-ffmpeg-output.txt" + +Write-Output "$args" > $Output +exit \ No newline at end of file diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.sh b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.sh new file mode 100644 index 0000000..b6cbfe0 --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffmpeg.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -o errexit -o noclobber -o pipefail -o nounset + +output=/tmp/test-ffmpeg-output.txt + +echo "$@" >| $output \ No newline at end of file diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.exe b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.exe new file mode 100644 index 0000000..60c6ac7 Binary files /dev/null and b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.exe differ diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.ps1 b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.ps1 new file mode 100644 index 0000000..e11cb1a --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.ps1 @@ -0,0 +1,6 @@ +$TempDir = [System.IO.Path]::GetTempPath() +$Output = "$TempDir\test-ffprobe-output.txt" + +Write-Output "$args" > $Output +Write-Host "1920,1080,25" +exit \ No newline at end of file diff --git a/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.sh b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.sh new file mode 100644 index 0000000..438d99f --- /dev/null +++ b/ffmpeg/src/test/resources/com/github/gtache/autosubtitle/ffmpeg/fake-ffprobe.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -o errexit -o noclobber -o pipefail -o nounset + +output=/tmp/test-ffprobe-output.txt + +echo "$@" >| $output +echo "1920,1080,25" \ No newline at end of file diff --git a/ffmpeg/src/test/resources/log4j2-test.xml b/ffmpeg/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..98cfd73 --- /dev/null +++ b/ffmpeg/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/MainModel.java b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/MainModel.java index ac1dc07..e69d43d 100644 --- a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/MainModel.java +++ b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/MainModel.java @@ -13,5 +13,5 @@ public interface MainModel { /** * @param index The index of the tab to select */ - void selectTab(int index); + void setSelectedTab(int index); } diff --git a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SetupModel.java b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SetupModel.java index 4352515..e384775 100644 --- a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SetupModel.java +++ b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SetupModel.java @@ -19,20 +19,6 @@ public interface SetupModel { */ void setSubtitleExtractorStatus(SetupStatus status); - /** - * @return whether the subtitle extractor is installed - */ - default boolean isSubtitleExtractorInstalled() { - return subtitleExtractorStatus().isInstalled(); - } - - /** - * @return whether an update is available for the subtitle extractor - */ - default boolean isSubtitleExtractorUpdateAvailable() { - return subtitleExtractorStatus() == SetupStatus.UPDATE_AVAILABLE; - } - /** * @return the progress of the subtitle extractor setup */ @@ -69,20 +55,6 @@ public interface SetupModel { */ void setVideoConverterStatus(SetupStatus status); - /** - * @return whether the video converter is installed - */ - default boolean isVideoConverterInstalled() { - return videoConverterStatus().isInstalled(); - } - - /** - * @return whether an update is available for the video converter - */ - default boolean isVideoConverterUpdateAvailable() { - return videoConverterStatus() == SetupStatus.UPDATE_AVAILABLE; - } - /** * @return the progress of the video converter setup */ @@ -119,20 +91,6 @@ public interface SetupModel { */ void setTranslatorStatus(SetupStatus status); - /** - * @return whether the translator is installed - */ - default boolean isTranslatorInstalled() { - return translatorStatus().isInstalled(); - } - - /** - * @return whether an update is available for the translator - */ - default boolean isTranslatorUpdateAvailable() { - return translatorStatus() == SetupStatus.UPDATE_AVAILABLE; - } - /** * @return the progress of the translator setup */ diff --git a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SubtitlesController.java b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SubtitlesController.java new file mode 100644 index 0000000..d6987fc --- /dev/null +++ b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SubtitlesController.java @@ -0,0 +1,44 @@ +package com.github.gtache.autosubtitle.gui; + +import com.github.gtache.autosubtitle.Language; + +import java.nio.file.Path; + +/** + * Controller for the subtitles view + */ +public interface SubtitlesController { + + /** + * Selects the given language for edition + * + * @param language The language + */ + void selectLanguage(final Language language); + + /** + * Deletes a language + * + * @param language The language + */ + void deleteLanguage(final Language language); + + /** + * Saves the subtitles to the given path + * + * @param file The output path + */ + void saveSubtitles(final Path file); + + /** + * Loads a subtitles file + * + * @param file The path to the file + */ + void loadSubtitles(final Path file); + + /** + * @return the model + */ + SubtitlesModel model(); +} diff --git a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SubtitlesModel.java b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SubtitlesModel.java new file mode 100644 index 0000000..1dec1d9 --- /dev/null +++ b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/SubtitlesModel.java @@ -0,0 +1,146 @@ +package com.github.gtache.autosubtitle.gui; + +import com.github.gtache.autosubtitle.Language; +import com.github.gtache.autosubtitle.subtitle.Subtitle; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; + +import java.util.List; +import java.util.Map; + +/** + * Model for the subtitles view + * + * @param The type of subtitle + * @param The type of subtitle collection + */ +public interface SubtitlesModel> { + + /** + * @return The list of available video languages + */ + List availableVideoLanguages(); + + /** + * @return the video language + */ + Language videoLanguage(); + + /** + * Sets the video language + * + * @param language The new language + */ + void setVideoLanguage(Language language); + + /** + * @return The list of available translations languages + */ + List availableTranslationsLanguage(); + + /** + * @return The list of selected translations languages + */ + List selectedTranslationsLanguages(); + + /** + * @return The currently selected language + */ + Language selectedLanguage(); + + /** + * @param language The new selected language + */ + void setSelectedLanguage(Language language); + + /** + * @return The mapping of language to subtitles + */ + Map collections(); + + /** + * @return The currently selected collection + */ + U selectedCollection(); + + /** + * @param collection The new selected collection + */ + void setSelectedCollection(U collection); + + /** + * @return The mapping of language to subtitles + */ + Map originalCollections(); + + /** + * @return The list of selected subtitles + */ + List selectedSubtitles(); + + /** + * @return The currently selected subtitle + */ + T selectedSubtitle(); + + /** + * @param subtitle The new selected subtitle + */ + void setSelectedSubtitle(T subtitle); + + /** + * @return Whether the user can load subtitles + */ + boolean canLoadSubtitles(); + + /** + * @param canLoadSubtitles Whether the user can load subtitles + */ + void setCanLoadSubtitles(boolean canLoadSubtitles); + + /** + * @return Whether the user can add subtitles + */ + boolean canAddSubtitle(); + + /** + * @param canAddSubtitle Whether the user can add subtitles + */ + void setCanAddSubtitle(boolean canAddSubtitle); + + /** + * @return Whether the user can reset subtitles + */ + boolean canResetSubtitles(); + + /** + * @param canResetSubtitles Whether the user can reset subtitles + */ + void setCanResetSubtitles(boolean canResetSubtitles); + + /** + * @return Whether the user can save subtitles + */ + boolean canSaveSubtitles(); + + /** + * @return Whether subtitles are currently being translated + */ + boolean isTranslating(); + + /** + * @param translating Whether subtitles are currently being translated + */ + void setTranslating(boolean translating); + + /** + * @return Whether the user can edit the table + */ + boolean canEditTable(); + + /** + * Sets whether the user can edit the table + * + * @param canEditTable Whether the user can edit the table + */ + void setCanEditTable(boolean canEditTable); +} diff --git a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkController.java b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkController.java index a066374..c9521a0 100644 --- a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkController.java +++ b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkController.java @@ -18,21 +18,7 @@ public interface WorkController { * @param file The path to the video */ void loadVideo(final Path file); - - /** - * Saves the subtitles to the given path - * - * @param file The output path - */ - void saveSubtitles(final Path file); - - /** - * Loads a subtitles file - * - * @param file The path to the file - */ - void loadSubtitles(final Path file); - + /** * @return The model */ diff --git a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkModel.java b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkModel.java index e5fb221..1e6d01a 100644 --- a/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkModel.java +++ b/gui/api/src/main/java/com/github/gtache/autosubtitle/gui/WorkModel.java @@ -1,13 +1,9 @@ package com.github.gtache.autosubtitle.gui; -import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.Video; -import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel; -import java.util.List; - /** * Model for the main view */ @@ -18,6 +14,11 @@ public interface WorkModel { */ Video video(); + /** + * @param video The new video + */ + void setVideo(Video video); + /** * @return The current extraction model */ @@ -28,56 +29,6 @@ public interface WorkModel { */ void setExtractionModel(ExtractionModel model); - /** - * @return The current subtitle collection - */ - SubtitleCollection subtitleCollection(); - - /** - * @return The current list of subtitles - */ - List subtitles(); - - /** - * @return The current text - */ - String text(); - - /** - * @return The original extracted subtitles (used to reset) - */ - List originalSubtitles(); - - /** - * @return The currently selected subtitle - */ - EditableSubtitle selectedSubtitle(); - - /** - * @return The list of available video languages - */ - List availableVideoLanguages(); - - /** - * @return The list of available translations languages - */ - List availableTranslationsLanguage(); - - /** - * @return The video language - */ - Language videoLanguage(); - - /** - * @param language The video language - */ - void setVideoLanguage(Language language); - - /** - * @return The list of selected translations - */ - List translations(); - /** * @return The current status */ @@ -97,4 +48,36 @@ public interface WorkModel { * @param progress The new progress */ void setProgress(double progress); + + /** + * @return Whether the user can extract subtitles + */ + boolean canExtract(); + + /** + * @return Whether the user can export subtitles + */ + boolean canExport(); + + /** + * @param canExport Whether the user can export subtitles + */ + void setCanExport(boolean canExport); + + /** + * @return Whether the progress bar and label are currently visible + */ + boolean isProgressVisible(); + + /** + * @return The last extracted collection + */ + SubtitleCollection extractedCollection(); + + /** + * Sets the last extracted collection + * + * @param collection The last extracted collection + */ + void setExtractedCollection(SubtitleCollection collection); } diff --git a/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/CombinedResourceBundle.java b/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/CombinedResourceBundle.java index f55c86a..04b9c63 100644 --- a/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/CombinedResourceBundle.java +++ b/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/CombinedResourceBundle.java @@ -1,35 +1,52 @@ package com.github.gtache.autosubtitle.gui.impl; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.ResourceBundle; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.*; /** * Combines multiple resource bundles */ public class CombinedResourceBundle extends ResourceBundle { + private static final Logger logger = LogManager.getLogger(CombinedResourceBundle.class); private final Map resources; + private final Locale locale; - public CombinedResourceBundle(final ResourceBundle... bundles) { - this(Arrays.asList(bundles)); + public CombinedResourceBundle(final ResourceBundle... resourceBundles) { + this(Arrays.asList(resourceBundles)); } - public CombinedResourceBundle(final Iterable bundles) { + public CombinedResourceBundle(final List resourceBundles) { + final var filteredBundles = resourceBundles.stream().filter(Objects::nonNull).toList(); + if (filteredBundles.size() != resourceBundles.size()) { + logger.warn("There was one or more null bundles in the inner bundles"); + } + if (filteredBundles.isEmpty()) { + throw new IllegalArgumentException("The bundle should contain at least one bundle"); + } this.resources = new HashMap<>(); - bundles.forEach(rb -> rb.getKeys().asIterator().forEachRemaining(key -> resources.put(key, rb.getString(key)))); + filteredBundles.forEach(r -> r.keySet().forEach(s -> resources.put(s, r.getString(s)))); + this.locale = filteredBundles.getFirst().getLocale(); } @Override - protected Object handleGetObject(final String key) { - return resources.get(key); + public Object handleGetObject(final String key) { + if (resources.containsKey(key)) { + return resources.get(key); + } else { + throw new MissingResourceException(key + " not found", "CombinedResourceBundle", key); + } } @Override public Enumeration getKeys() { return Collections.enumeration(resources.keySet()); } + + @Override + public Locale getLocale() { + return locale; + } } diff --git a/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/spi/SubtitlesBundleProvider.java b/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/spi/SubtitlesBundleProvider.java new file mode 100644 index 0000000..409cb4e --- /dev/null +++ b/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/spi/SubtitlesBundleProvider.java @@ -0,0 +1,9 @@ +package com.github.gtache.autosubtitle.gui.impl.spi; + +import java.util.spi.ResourceBundleProvider; + +/** + * Provider for SubtitlesBundle + */ +public interface SubtitlesBundleProvider extends ResourceBundleProvider { +} diff --git a/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/spi/SubtitlesBundleProviderImpl.java b/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/spi/SubtitlesBundleProviderImpl.java new file mode 100644 index 0000000..5657769 --- /dev/null +++ b/gui/core/src/main/java/com/github/gtache/autosubtitle/gui/impl/spi/SubtitlesBundleProviderImpl.java @@ -0,0 +1,9 @@ +package com.github.gtache.autosubtitle.gui.impl.spi; + +import java.util.spi.AbstractResourceBundleProvider; + +/** + * Implementation of {@link SubtitlesBundleProvider} + */ +public class SubtitlesBundleProviderImpl extends AbstractResourceBundleProvider implements SubtitlesBundleProvider { +} diff --git a/gui/core/src/main/java/com/github/gtache/autosubtitle/modules/gui/impl/GuiCoreModule.java b/gui/core/src/main/java/com/github/gtache/autosubtitle/modules/gui/impl/GuiCoreModule.java index dff92ff..eaafa50 100644 --- a/gui/core/src/main/java/com/github/gtache/autosubtitle/modules/gui/impl/GuiCoreModule.java +++ b/gui/core/src/main/java/com/github/gtache/autosubtitle/modules/gui/impl/GuiCoreModule.java @@ -23,6 +23,7 @@ public final class GuiCoreModule { ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.SetupBundle"), ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.WorkBundle"), ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.ParametersBundle"), + ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.SubtitlesBundle"), ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MediaBundle")); } diff --git a/gui/core/src/main/java/module-info.java b/gui/core/src/main/java/module-info.java index 9b163ce..48e0010 100644 --- a/gui/core/src/main/java/module-info.java +++ b/gui/core/src/main/java/module-info.java @@ -13,6 +13,7 @@ import com.github.gtache.autosubtitle.gui.impl.spi.WorkBundleProviderImpl; module com.github.gtache.autosubtitle.gui.core { requires transitive com.github.gtache.autosubtitle.gui.api; requires transitive com.github.gtache.autosubtitle.core; + requires org.apache.logging.log4j; exports com.github.gtache.autosubtitle.gui.impl; exports com.github.gtache.autosubtitle.gui.impl.spi; exports com.github.gtache.autosubtitle.modules.gui.impl; diff --git a/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/SubtitlesBundle.properties b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/SubtitlesBundle.properties new file mode 100644 index 0000000..dc37480 --- /dev/null +++ b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/SubtitlesBundle.properties @@ -0,0 +1,14 @@ +subtitles.button.load.label=Load subtitles... +subtitles.button.reset.label=Reset subtitles +subtitles.button.subtitles.save.label=Save subtitles... +subtitles.export.error.label=Error during the export : {0} +subtitles.export.error.title=Error exporting +subtitles.language.label=Video language +subtitles.load.error.label=Error loading subtitles : {0} +subtitles.load.error.title=Error loading +subtitles.save.error.label=Error saving subtitles : {0} +subtitles.save.error.title=Error saving +subtitles.table.column.from.label=From +subtitles.table.column.text.label=Text +subtitles.table.column.to.label=To +subtitles.translate.label=Automatic translations \ No newline at end of file diff --git a/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/SubtitlesBundle_fr.properties b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/SubtitlesBundle_fr.properties new file mode 100644 index 0000000..b3369b1 --- /dev/null +++ b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/SubtitlesBundle_fr.properties @@ -0,0 +1,14 @@ +subtitles.button.load.label=Charger des sous-titres... +subtitles.button.reset.label=R\u00E9initialiser les sous-titres +subtitles.button.subtitles.save.label=Sauvegarder les sous-titres... +subtitles.export.error.label=Erreur durant l''export : {0} +subtitles.export.error.title=Erreur d'export +subtitles.language.label=Langage de la vid\u00E9o +subtitles.load.error.label=Erreur de chargement des sous-titres : {0} +subtitles.load.error.title=Erreur de chargement +subtitles.save.error.label=Erreur de sauvegarde des sous-titres : {0} +subtitles.save.error.title=Erreur lors de la sauvegarde +subtitles.table.column.from.label=De +subtitles.table.column.text.label=Texte +subtitles.table.column.to.label=\u00C0 +subtitles.translate.label=Traductions diff --git a/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle.properties b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle.properties index 51db763..3fef742 100644 --- a/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle.properties +++ b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle.properties @@ -4,27 +4,13 @@ work.button.export.soft.label=Export video... work.button.export.soft.tooltip=Adds the subtitles to the video. This allows a video to have multiple subtitles and to enable them at will. work.button.extract.label=Extract subtitles work.button.file.label=Open video... -work.button.load.label=Load subtitles... -work.button.reset.label=Reset subtitles -work.button.subtitles.save.label=Save subtitles... work.export.error.label=Error during the export : {0} work.export.error.title=Error exporting work.extract.error.label=Error extracting subtitles : {0} work.extract.error.title=Error extracting -work.language.label=Video language -work.load.subtitles.error.label=Error loading subtitles : {0} -work.load.subtitles.error.title=Error loading work.load.video.error.label=Error loading video : {0} work.load.video.error.title=Error loading -work.save.subtitles.error.label=Error saving subtitles : {0} -work.save.subtitles.error.title=Error saving -work.save.subtitles.missing.converter.label=No converter found for {0} -work.save.subtitles.missing.converter.title=No converter found work.status.exporting.label=Exporting... work.status.extracting.label=Extracting... work.status.idle.label=Idle -work.status.translating.label=Translating... -work.table.column.from.label=From -work.table.column.text.label=Text -work.table.column.to.label=To -work.translate.label=Automatic translations \ No newline at end of file +work.status.translating.label=Translating... \ No newline at end of file diff --git a/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle_fr.properties b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle_fr.properties index 00fb672..ba0a06e 100644 --- a/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle_fr.properties +++ b/gui/core/src/main/resources/com/github/gtache/autosubtitle/gui/impl/WorkBundle_fr.properties @@ -4,25 +4,13 @@ work.button.export.soft.label=Exporter la vid\u00E9o... work.button.export.soft.tooltip=Ajoute les sous-titres \u00E0 la vid\u00E9o. Cela permet d'avoir plusieurs pistes de sous-titres dans une m\u00EAme vid\u00E9o et de les activer comme d\u00E9sir\u00E9. work.button.extract.label=Extraire les sous-titres work.button.file.label=Ouvrir une vid\u00E9o... -work.button.load.label=Charger des sous-titres... -work.button.reset.label=R\u00E9initialiser les sous-titres -work.button.subtitles.save.label=Sauvegarder les sous-titres... work.export.error.label=Erreur durant l''export : {0} work.export.error.title=Erreur d'export work.extract.error.label=Erreur durant l''extraction des sous-titres : {0} work.extract.error.title=Erreur d'extraction -work.language.label=Language de la vid\u00E9o -work.load.subtitles.error.label=Erreur de chargement des sous-titres : {0} -work.load.subtitles.error.title=Erreur de chargement work.load.video.error.label=Erreur lors du chargement de la vid\u00E9o : {0} work.load.video.error.title=Erreur de chargement -work.save.subtitles.missing.converter.label=Aucun convertisseur trouv\u00E9 pour {0} -work.save.subtitles.missing.converter.title=Aucun convertisseur trouv\u00E9 work.status.exporting.label=Exportation en cours... work.status.extracting.label=Extraction en cours... work.status.idle.label=Idle work.status.translating.label=Traduction en cours... -work.table.column.from.label=De -work.table.column.text.label=Texte -work.table.column.to.label=\u00C0 -work.translate.label=Traductions automatiques diff --git a/gui/core/src/test/java/com/github/gtache/autosubtitle/gui/impl/TestCombinedResourceBundle.java b/gui/core/src/test/java/com/github/gtache/autosubtitle/gui/impl/TestCombinedResourceBundle.java new file mode 100644 index 0000000..9f6ac4e --- /dev/null +++ b/gui/core/src/test/java/com/github/gtache/autosubtitle/gui/impl/TestCombinedResourceBundle.java @@ -0,0 +1,54 @@ +package com.github.gtache.autosubtitle.gui.impl; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TestCombinedResourceBundle { + + private static final ResourceBundle BUNDLE = new CombinedResourceBundle( + ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MultiBundle", Locale.FRENCH), + ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.impl.MultiBundleTwo", Locale.FRENCH)); + + @Test + void testIllegal() { + assertThrows(IllegalArgumentException.class, CombinedResourceBundle::new); + assertThrows(IllegalArgumentException.class, () -> new CombinedResourceBundle(null, null)); + } + + @Test + void testWorks() { + assertEquals("deux", BUNDLE.getString("a")); + assertEquals("deux", BUNDLE.getString("b")); + assertEquals("trois", BUNDLE.getString("c")); + assertEquals("un", BUNDLE.getString("d")); + assertEquals(Arrays.asList("a", "b", "c", "d"), Collections.list(BUNDLE.getKeys())); + } + + @Test + void testNotFound() { + assertThrows(MissingResourceException.class, () -> BUNDLE.getString("e")); + } + + @Test + void testLocale() { + final var bundle = mock(ResourceBundle.class); + when(bundle.keySet()).thenReturn(Set.of()); + when(bundle.getString(anyString())).thenReturn(""); + final var locale = mock(Locale.class); + when(bundle.getLocale()).thenReturn(locale); + final var combined = new CombinedResourceBundle(bundle); + assertEquals(locale, combined.getLocale()); + } +} diff --git a/gui/core/src/test/java/com/github/gtache/autosubtitle/modules/gui/impl/TestGuiCoreModule.java b/gui/core/src/test/java/com/github/gtache/autosubtitle/modules/gui/impl/TestGuiCoreModule.java new file mode 100644 index 0000000..cc81467 --- /dev/null +++ b/gui/core/src/test/java/com/github/gtache/autosubtitle/modules/gui/impl/TestGuiCoreModule.java @@ -0,0 +1,34 @@ +package com.github.gtache.autosubtitle.modules.gui.impl; + +import com.github.gtache.autosubtitle.gui.impl.CombinedResourceBundle; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TestGuiCoreModule { + + @Test + void testBundle() { + assertInstanceOf(CombinedResourceBundle.class, GuiCoreModule.providesBundle()); + } + + @Test + void testPlayImage() { + assertTrue(GuiCoreModule.providesPlayImage().length > 0); + } + + @Test + void testPauseImage() { + assertTrue(GuiCoreModule.providesPauseImage().length > 0); + } + + @Test + void testFontFamily() { + assertEquals("Arial", GuiCoreModule.providesFontFamily()); + } + + @Test + void testFontSize() { + assertEquals(12, GuiCoreModule.providesFontSize()); + } +} diff --git a/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle.properties b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle.properties new file mode 100644 index 0000000..65cb097 --- /dev/null +++ b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle.properties @@ -0,0 +1,3 @@ +a=one +b=two +c=three \ No newline at end of file diff --git a/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo.properties b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo.properties new file mode 100644 index 0000000..02a0253 --- /dev/null +++ b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo.properties @@ -0,0 +1,2 @@ +a=two +d=one \ No newline at end of file diff --git a/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo_de.properties b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo_de.properties new file mode 100644 index 0000000..da5e8a2 --- /dev/null +++ b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo_de.properties @@ -0,0 +1,2 @@ +a=zwei +d=eins \ No newline at end of file diff --git a/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo_fr.properties b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo_fr.properties new file mode 100644 index 0000000..1c078f3 --- /dev/null +++ b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundleTwo_fr.properties @@ -0,0 +1,2 @@ +a=deux +d=un \ No newline at end of file diff --git a/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle_de.properties b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle_de.properties new file mode 100644 index 0000000..eadf97f --- /dev/null +++ b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle_de.properties @@ -0,0 +1,3 @@ +a=eins +b=zwei +c=drei \ No newline at end of file diff --git a/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle_fr.properties b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle_fr.properties new file mode 100644 index 0000000..a35eefe --- /dev/null +++ b/gui/core/src/test/resources/com/github/gtache/autosubtitle/gui/impl/MultiBundle_fr.properties @@ -0,0 +1,3 @@ +a=un +b=deux +c=trois \ No newline at end of file diff --git a/gui/fx/pom.xml b/gui/fx/pom.xml index 40ebd2e..87a03b1 100644 --- a/gui/fx/pom.xml +++ b/gui/fx/pom.xml @@ -13,7 +13,8 @@ 11.2.1 - 22.0.1 + 22.0.2 + 4.0.18 @@ -44,6 +45,23 @@ controlsfx ${controlsfx.version} + + org.testfx + testfx-core + ${testfx.version} + test + + + org.testfx + testfx-junit5 + ${testfx.version} + test + + + org.apache.logging.log4j + log4j-core + test + \ No newline at end of file diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java index 1fb24a0..dbf9501 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java @@ -27,18 +27,19 @@ public class ColonTimeFormatter implements TimeFormatter { public String format(final long millis) { final var secondsInMinute = 60; final var secondsInHour = secondsInMinute * 60; - var intDuration = (int) millis / 1000; - final var durationHours = intDuration / secondsInHour; + var secondsDuration = millis / 1000L; + final var durationHours = secondsDuration / secondsInHour; if (durationHours > 0) { - intDuration -= durationHours * secondsInHour; + secondsDuration -= durationHours * secondsInHour; } - final var durationMinutes = intDuration / secondsInMinute; - final var durationSeconds = intDuration - durationHours * secondsInHour - - durationMinutes * secondsInMinute; + final var durationMinutes = secondsDuration / secondsInMinute; + secondsDuration -= durationMinutes * secondsInMinute; + final var durationSeconds = secondsDuration; + final var durationMillis = millis % 1000L; if (durationHours > 0) { - return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds); + return "%d:%02d:%02d.%03d".formatted(durationHours, durationMinutes, durationSeconds, durationMillis); } else { - return String.format("%02d:%02d", durationMinutes, durationSeconds); + return "%02d:%02d.%03d".formatted(durationMinutes, durationSeconds, durationMillis); } } @@ -48,14 +49,20 @@ public class ColonTimeFormatter implements TimeFormatter { final var secondsInMinute = 60; final var secondsInHour = secondsInMinute * 60; return switch (split.length) { - case 1 -> toLong(split[0]) * 1000; - case 2 -> (toLong(split[0]) * secondsInMinute + toLong(split[1])) * 1000; - case 3 -> (toLong(split[0]) * secondsInHour + toLong(split[1]) * secondsInMinute + toLong(split[2])) * 1000; + case 1 -> parseSecondsMillis(split[0]); + case 2 -> toLong(split[0]) * secondsInMinute * 1000 + parseSecondsMillis(split[1]); + case 3 -> + (toLong(split[0]) * secondsInHour + toLong(split[1]) * secondsInMinute) * 1000 + parseSecondsMillis(split[2]); default -> 0; }; } - private long toLong(final String time) { + private static long parseSecondsMillis(final String time) { + final var split = time.split("\\."); + return toLong(split[0]) * 1000 + (split.length > 1 ? toLong(split[1]) : 0); + } + + private static long toLong(final String time) { if (time.startsWith("0")) { return Long.parseLong(time.substring(1)); } else { diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXBinder.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXBinder.java new file mode 100644 index 0000000..0fc4a99 --- /dev/null +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXBinder.java @@ -0,0 +1,13 @@ +package com.github.gtache.autosubtitle.gui.fx; + +/** + * Binds multiple models together + */ +@FunctionalInterface +public interface FXBinder { + + /** + * Creates the bindings between the models + */ + void createBindings(); +} diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java index cf72ea2..14df25b 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java @@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.gui.fx; import com.github.gtache.autosubtitle.gui.MainController; import javafx.fxml.FXML; import javafx.scene.control.TabPane; +import javafx.stage.Window; import javax.inject.Inject; import javax.inject.Singleton; @@ -12,7 +13,7 @@ import java.util.Objects; * FX implementation of {@link MainController} */ @Singleton -public class FXMainController implements MainController { +public class FXMainController extends AbstractFXController implements MainController { @FXML private TabPane tabPane; @@ -25,18 +26,23 @@ public class FXMainController implements MainController { } @FXML - private void initialize() { - tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.selectTab(newValue.intValue())); + void initialize() { + tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> model.setSelectedTab(newValue.intValue())); model.selectedTabProperty().addListener((observable, oldValue, newValue) -> tabPane.getSelectionModel().select(newValue.intValue())); } @Override public void selectTab(final int index) { - model.selectTab(index); + model.setSelectedTab(index); } @Override public FXMainModel model() { return model; } + + @Override + protected Window window() { + return tabPane.getScene().getWindow(); + } } diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java index 04a38a6..7aa546b 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java @@ -26,7 +26,7 @@ public class FXMainModel implements MainModel { } @Override - public void selectTab(final int index) { + public void setSelectedTab(final int index) { selectedTab.set(index); } diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java index 3176cea..db6b516 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java @@ -10,7 +10,7 @@ import java.util.Objects; * Binds the media model */ @Singleton -public class FXMediaBinder { +public class FXMediaBinder implements FXBinder { private final FXWorkModel workModel; private final FXMediaModel mediaModel; @@ -21,6 +21,7 @@ public class FXMediaBinder { this.mediaModel = Objects.requireNonNull(mediaModel); } + @Override public void createBindings() { mediaModel.videoProperty().bindBidirectional(workModel.videoProperty()); Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles()); diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java index 07b8b39..5a252e1 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java @@ -5,12 +5,10 @@ import com.github.gtache.autosubtitle.gui.MediaController; import com.github.gtache.autosubtitle.gui.TimeFormatter; import com.github.gtache.autosubtitle.modules.gui.impl.Pause; import com.github.gtache.autosubtitle.modules.gui.impl.Play; -import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel; import javafx.application.Platform; import javafx.beans.binding.Bindings; -import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Cursor; @@ -34,9 +32,7 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; import javax.inject.Singleton; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import static java.util.Objects.requireNonNull; @@ -69,8 +65,6 @@ public class FXMediaController implements MediaController { private final Image playImage; private final Image pauseImage; - private final List startTimes; - private boolean wasPlaying; @Inject @@ -81,11 +75,10 @@ public class FXMediaController implements MediaController { this.timeFormatter = requireNonNull(timeFormatter); this.playImage = requireNonNull(playImage); this.pauseImage = requireNonNull(pauseImage); - this.startTimes = new ArrayList<>(); } @FXML - private void initialize() { + void initialize() { volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) (model.volume() * 100)), model.volumeProperty())); playLabel.textProperty().bind(Bindings.createStringBinding(() -> timeFormatter.format(model.position(), model.duration()), model.positionProperty(), model.durationProperty())); model.positionProperty().bindBidirectional(playSlider.valueProperty()); @@ -115,13 +108,10 @@ public class FXMediaController implements MediaController { loadFileVideo(file.path()); } else { logger.error("Unsupported video type : {}", newValue); + Platform.runLater(() -> model.setVideo(null)); } }); - model.subtitles().addListener((ListChangeListener) c -> { - startTimes.clear(); - model.subtitles().stream().mapToLong(Subtitle::start).forEach(startTimes::add); - }); bindPlayButton(); binder.createBindings(); } @@ -130,8 +120,8 @@ public class FXMediaController implements MediaController { final var media = new Media(fileVideoPath.toUri().toString()); final var player = new MediaPlayer(media); player.statusProperty().addListener((observable12, oldValue1, newValue1) -> - logger.info("New status: {}", newValue1)); - player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(oldTime.toMillis(), newTime.toMillis())); + logger.debug("New player status: {}", newValue1)); + player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(newTime.toMillis())); playSlider.setOnMousePressed(e -> { wasPlaying = model.isPlaying(); model.setIsPlaying(false); @@ -155,33 +145,37 @@ public class FXMediaController implements MediaController { playSlider.setMax(model.duration()); playSlider.setValue(0L); videoView.setMediaPlayer(player); + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(3000L); + } catch (final InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + Platform.runLater(() -> { + final var status = player.getStatus(); + if (status == null || status == MediaPlayer.Status.UNKNOWN) { + logger.warn("Reloading video {} because player state is unknown or null", fileVideoPath); + loadFileVideo(fileVideoPath); + } + }); + }); } - private void currentTimeChanged(final double oldMillis, final double millis) { + private void currentTimeChanged(final double millis) { + final var longMillis = (long) millis; playSlider.setValue(millis); final var subtitleLabels = stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).toList(); - subtitleLabels.stream().filter(s -> !s.subtitle().isShowing((long) millis)).forEach(sl -> stackPane.getChildren().remove(sl)); - final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing((long) millis)).collect(Collectors.toSet()); - + subtitleLabels.stream().filter(s -> !s.subtitle().isShowing(longMillis)).forEach(sl -> stackPane.getChildren().remove(sl)); + final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing(longMillis)).collect(Collectors.toSet()); + //TODO optimize? model.subtitles().forEach(s -> { - if (!containedSubtitles.contains(s)) { - logger.info("Adding label {} at {}", s, millis); + if (!containedSubtitles.contains(s) && s.isShowing(longMillis)) { final var label = createDraggableLabel(s); stackPane.getChildren().add(label); } }); } - private void currentTimeChangedOptimized(final double oldMillis, final double millis) { - final var forward = oldMillis <= millis; - - var index = Collections.binarySearch(startTimes, (long) millis); - if (index < 0) { - index = forward ? -(index + 1) : -(index + 2); - } - //TODO - } - private void bindPlayButton() { playButton.disableProperty().bind(model.videoProperty().isNull()); playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> { @@ -217,6 +211,7 @@ public class FXMediaController implements MediaController { public void seek(final long position) { if (videoView.getMediaPlayer() != null) { videoView.getMediaPlayer().seek(Duration.millis(position)); + currentTimeChanged(position); } } diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java index 385cbba..99f1786 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java @@ -102,12 +102,12 @@ public class FXMediaModel implements MediaModel { this.position.set(position); } + LongProperty positionProperty() { + return position; + } + @Override public ObservableList subtitles() { return subtitles; } - - LongProperty positionProperty() { - return position; - } } diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java index e77035e..905afea 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java @@ -50,7 +50,7 @@ public class FXParametersController extends AbstractFXController implements Para } @FXML - private void initialize() { + void initialize() { extractionModelCombobox.setItems(model.availableExtractionModels()); extractionModelCombobox.valueProperty().bindBidirectional(model.extractionModelProperty()); diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java index 1be5322..9312420 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java @@ -97,7 +97,7 @@ public class FXSetupController extends AbstractFXController implements SetupCont } @FXML - private void initialize() { + void initialize() { statusMap.put(converterManager, model.videoConverterStatusProperty()); statusMap.put(extractorManager, model.subtitleExtractorStatusProperty()); statusMap.put(translatorManager, model.translatorStatusProperty()); diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java index 200b49c..5b9e6ac 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java @@ -2,11 +2,8 @@ package com.github.gtache.autosubtitle.gui.fx; import com.github.gtache.autosubtitle.gui.SetupModel; import com.github.gtache.autosubtitle.setup.SetupStatus; -import javafx.beans.binding.Bindings; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; @@ -22,45 +19,26 @@ import javax.inject.Singleton; public class FXSetupModel implements SetupModel { private final ObjectProperty subtitleExtractorStatus; - private final ReadOnlyBooleanWrapper subtitleExtractorInstalled; - private final ReadOnlyBooleanWrapper subtitleExtractorUpdateAvailable; private final DoubleProperty subtitleExtractorSetupProgress; private final StringProperty subtitleExtractorSetupProgressLabel; private final ObjectProperty videoConverterStatus; - private final ReadOnlyBooleanWrapper videoConverterInstalled; - private final ReadOnlyBooleanWrapper videoConverterUpdateAvailable; private final DoubleProperty videoConverterSetupProgress; private final StringProperty videoConverterSetupProgressLabel; private final ObjectProperty translatorStatus; - private final ReadOnlyBooleanWrapper translatorInstalled; - private final ReadOnlyBooleanWrapper translatorUpdateAvailable; private final DoubleProperty translatorSetupProgress; private final StringProperty translatorSetupProgressLabel; @Inject FXSetupModel() { this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED); - this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false); - this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false); this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(-2); this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty(""); this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED); - this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false); - this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false); this.videoConverterSetupProgress = new SimpleDoubleProperty(-2); this.videoConverterSetupProgressLabel = new SimpleStringProperty(""); this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED); - this.translatorInstalled = new ReadOnlyBooleanWrapper(false); - this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false); this.translatorSetupProgress = new SimpleDoubleProperty(-2); this.translatorSetupProgressLabel = new SimpleStringProperty(""); - - subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus)); - videoConverterInstalled.bind(Bindings.createBooleanBinding(() -> videoConverterStatus.get().isInstalled(), videoConverterStatus)); - translatorInstalled.bind(Bindings.createBooleanBinding(() -> translatorStatus.get().isInstalled(), translatorStatus)); - subtitleExtractorUpdateAvailable.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get() == SetupStatus.UPDATE_AVAILABLE, subtitleExtractorStatus)); - videoConverterUpdateAvailable.bind(Bindings.createBooleanBinding(() -> videoConverterStatus.get() == SetupStatus.UPDATE_AVAILABLE, videoConverterStatus)); - translatorUpdateAvailable.bind(Bindings.createBooleanBinding(() -> translatorStatus.get() == SetupStatus.UPDATE_AVAILABLE, translatorStatus)); } @Override @@ -77,20 +55,6 @@ public class FXSetupModel implements SetupModel { return subtitleExtractorStatus; } - @Override - public boolean isSubtitleExtractorInstalled() { - return subtitleExtractorInstalled.get(); - } - - ReadOnlyBooleanProperty subtitleExtractorInstalledProperty() { - return subtitleExtractorInstalled.getReadOnlyProperty(); - } - - @Override - public boolean isSubtitleExtractorUpdateAvailable() { - return subtitleExtractorUpdateAvailable.get(); - } - @Override public double subtitleExtractorSetupProgress() { return subtitleExtractorSetupProgress.get(); @@ -133,28 +97,6 @@ public class FXSetupModel implements SetupModel { return videoConverterStatus; } - ReadOnlyBooleanProperty subtitleExtractorUpdateAvailableProperty() { - return subtitleExtractorUpdateAvailable.getReadOnlyProperty(); - } - - @Override - public boolean isVideoConverterInstalled() { - return videoConverterInstalled.get(); - } - - ReadOnlyBooleanProperty videoConverterInstalledProperty() { - return videoConverterInstalled.getReadOnlyProperty(); - } - - @Override - public boolean isVideoConverterUpdateAvailable() { - return videoConverterUpdateAvailable.get(); - } - - ReadOnlyBooleanProperty videoConverterUpdateAvailableProperty() { - return videoConverterUpdateAvailable.getReadOnlyProperty(); - } - @Override public double videoConverterSetupProgress() { return videoConverterSetupProgress.get(); @@ -197,24 +139,6 @@ public class FXSetupModel implements SetupModel { return translatorStatus; } - @Override - public boolean isTranslatorInstalled() { - return translatorInstalled.get(); - } - - ReadOnlyBooleanProperty translatorInstalledProperty() { - return translatorInstalled.getReadOnlyProperty(); - } - - @Override - public boolean isTranslatorUpdateAvailable() { - return translatorUpdateAvailable.get(); - } - - ReadOnlyBooleanProperty translatorUpdateAvailableProperty() { - return translatorUpdateAvailable.getReadOnlyProperty(); - } - @Override public double translatorSetupProgress() { return translatorSetupProgress.get(); diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesBinder.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesBinder.java new file mode 100644 index 0000000..bda34c2 --- /dev/null +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesBinder.java @@ -0,0 +1,55 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.Language; +import com.github.gtache.autosubtitle.gui.WorkStatus; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl; +import javafx.beans.binding.Bindings; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Objects; + +/** + * Binds the subtitles model + */ +@Singleton +public class FXSubtitlesBinder implements FXBinder { + + private final FXWorkModel workModel; + private final FXSubtitlesModel subtitlesModel; + + @Inject + FXSubtitlesBinder(final FXWorkModel workModel, final FXSubtitlesModel subtitlesModel) { + this.workModel = Objects.requireNonNull(workModel); + this.subtitlesModel = Objects.requireNonNull(subtitlesModel); + } + + @Override + public void createBindings() { + subtitlesModel.canLoadSubtitlesProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE))); + subtitlesModel.canResetSubtitlesProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE))); + subtitlesModel.canAddSubtitleProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)).and(subtitlesModel.videoLanguageProperty().isNotEqualTo(Language.AUTO))); + subtitlesModel.canEditTableProperty().bind(workModel.videoProperty().isNotNull().and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE)).and(subtitlesModel.videoLanguageProperty().isNotEqualTo(Language.AUTO))); + + workModel.selectedSubtitleProperty().bind(subtitlesModel.selectedSubtitleProperty()); + workModel.canExportProperty().bind(Bindings.isNotEmpty(subtitlesModel.collections()).and(workModel.statusProperty().isEqualTo(WorkStatus.IDLE))); + workModel.videoLanguageProperty().bind(subtitlesModel.videoLanguageProperty()); + + subtitlesModel.translatingProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + workModel.statusProperty().set(WorkStatus.TRANSLATING); + } else { + workModel.statusProperty().set(WorkStatus.IDLE); + } + }); + + workModel.extractedCollectionProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + subtitlesModel.collections().put(newValue.language(), new ObservableSubtitleCollectionImpl(newValue)); + } + }); + + Bindings.bindContent(workModel.collections(), subtitlesModel.collections()); + Bindings.bindContent(workModel.subtitles(), subtitlesModel.selectedSubtitles()); + } +} diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesController.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesController.java new file mode 100644 index 0000000..db151d6 --- /dev/null +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesController.java @@ -0,0 +1,322 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.Language; +import com.github.gtache.autosubtitle.Translator; +import com.github.gtache.autosubtitle.gui.SubtitlesController; +import com.github.gtache.autosubtitle.gui.TimeFormatter; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; +import com.github.gtache.autosubtitle.subtitle.SubtitleImporterExporter; +import com.github.gtache.autosubtitle.subtitle.converter.ParseException; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.MapChangeListener; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.input.KeyCode; +import javafx.stage.FileChooser; +import javafx.stage.Window; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.controlsfx.control.PrefixSelectionComboBox; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; + +import static java.util.Objects.requireNonNull; + +/** + * FX implementation of {@link SubtitlesController} + */ +@Singleton +public class FXSubtitlesController extends AbstractFXController implements SubtitlesController { + + private static final Logger logger = LogManager.getLogger(FXSubtitlesController.class); + private static final String ARCHIVE = "Archive"; + private static final String ALL_SUPPORTED = "All supported"; + + @FXML + private ResourceBundle resources; + @FXML + private Button loadButton; + @FXML + private Button resetButton; + @FXML + private Button saveButton; + @FXML + private Button addButton; + @FXML + private PrefixSelectionComboBox languageCombobox; + @FXML + private ComboBox translationsCombobox; + @FXML + private TabPane tabPane; + @FXML + private Tab mainSubtitlesTab; + @FXML + private TableView subtitlesTable; + @FXML + private TableColumn startColumn; + @FXML + private TableColumn endColumn; + @FXML + private TableColumn textColumn; + + private final FXSubtitlesModel model; + private final FXSubtitlesBinder binder; + private final SubtitleImporterExporter importerExporter; + private final TimeFormatter timeFormatter; + private final List subtitleExtensions; + private final Translator translator; + + @Inject + FXSubtitlesController(final FXSubtitlesModel model, final FXSubtitlesBinder binder, final SubtitleImporterExporter importerExporter, final TimeFormatter timeFormatter, + final Translator translator) { + this.model = requireNonNull(model); + this.binder = requireNonNull(binder); + this.importerExporter = requireNonNull(importerExporter); + this.timeFormatter = requireNonNull(timeFormatter); + this.subtitleExtensions = importerExporter.supportedSingleFileExtensions().stream().map(c -> "*." + c).sorted().toList(); + this.translator = requireNonNull(translator); + } + + @FXML + void initialize() { + addButton.disableProperty().bind(model.canAddSubtitleProperty().not()); + loadButton.disableProperty().bind(model.canLoadSubtitlesProperty().not()); + resetButton.disableProperty().bind(model.canResetSubtitlesProperty().not()); + saveButton.disableProperty().bind(model.canSaveSubtitlesProperty().not()); + + // Can't bind because tab calls updateDisabled which sets disableProperty + model.canEditTableProperty().addListener((observable, oldValue, newValue) -> subtitlesTable.setDisable(!newValue)); + + bindComboboxes(); + bindTable(); + mainSubtitlesTab.textProperty().bind(Bindings.createStringBinding(() -> model.videoLanguage().iso2(), model.videoLanguageProperty())); + model.collections().addListener((MapChangeListener) change -> { + manageTabs(); + }); + tabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + model.setSelectedCollection(model.collections().get(Language.getLanguage(newValue.getText()))); + if (oldValue != null) { + oldValue.setContent(null); + } + newValue.setContent(subtitlesTable); + } + }); + model.selectedLanguageProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + tabPane.getTabs().stream().filter(t -> Language.getLanguage(t.getText()) == newValue) + .findFirst().ifPresent(tab -> tabPane.getSelectionModel().select(tab)); + } + }); + + translationsCombobox.valueProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null && !model.collections().containsKey(newValue)) { + model.setTranslating(true); + CompletableFuture.supplyAsync(() -> translator.translate(model.collections().get(model.videoLanguage()), newValue)) + .whenCompleteAsync((r, t) -> { + if (t == null) { + loadCollection(r); + model.setSelectedCollection(model.collections().get(newValue)); + } else { + logger.error("Error while translating to {}", newValue, t); + } + model.setTranslating(false); + }, Platform::runLater); + } + }); + binder.createBindings(); + } + + private void bindTable() { + subtitlesTable.setItems(model.selectedSubtitles()); + subtitlesTable.setOnKeyPressed(e -> { + if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) { + editFocusedCell(); + } else if (e.getCode() == KeyCode.RIGHT || + e.getCode() == KeyCode.TAB) { + subtitlesTable.getSelectionModel().selectNext(); + e.consume(); + } else if (e.getCode() == KeyCode.LEFT) { + subtitlesTable.getSelectionModel().selectPrevious(); + e.consume(); + } else if (e.getCode() == KeyCode.DELETE) { + deleteSelectedSubtitles(); + e.consume(); + } + }); + startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter))); + startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start())); + startColumn.setOnEditCommit(e -> { + final var subtitle = e.getRowValue(); + subtitle.setStart(e.getNewValue()); + subtitlesTable.refresh(); + subtitlesTable.requestFocus(); + }); + endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter))); + endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end())); + endColumn.setOnEditCommit(e -> { + final var subtitle = e.getRowValue(); + subtitle.setEnd(e.getNewValue()); + subtitlesTable.refresh(); + subtitlesTable.requestFocus(); + }); + textColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + textColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue() == null ? null : param.getValue().content())); + textColumn.setOnEditCommit(e -> { + final var subtitle = e.getRowValue(); + subtitle.setContent(e.getNewValue()); + subtitlesTable.refresh(); + subtitlesTable.requestFocus(); + }); + + subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue)); + + } + + private void manageTabs() { + final var toRemove = new ArrayList(); + final var toAdd = new ArrayList(); + tabPane.getTabs().forEach(tab -> { + if (!model.collections().containsKey(Language.getLanguage(tab.getText()))) { + toRemove.add(tab); + } + }); + model.collections().forEach((language, collection) -> { + if (tabPane.getTabs().stream().noneMatch(t -> Language.getLanguage(t.getText()) == language)) { + toAdd.add(new Tab(language.iso2())); + } + }); + tabPane.getTabs().removeAll(toRemove); + tabPane.getTabs().addAll(toAdd); + tabPane.getTabs().sort(Comparator.comparing(Tab::getText)); + } + + private void bindComboboxes() { + languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty()); + languageCombobox.setItems(model.availableVideoLanguages()); + languageCombobox.setConverter(new LanguageStringConverter()); + translationsCombobox.setConverter(new LanguageStringConverter()); + translationsCombobox.setItems(model.availableTranslationsLanguage()); + } + + @FXML + private void resetButtonPressed() { + model.setSelectedCollection(model.originalCollections().get(model.selectedLanguage())); + } + + @FXML + private void addPressed() { + model.selectedCollection().subtitles().add(new ObservableSubtitleImpl("Enter text here...")); + } + + @FXML + private void loadPressed() { + final var filePicker = new FileChooser(); + final var archiveFilter = new FileChooser.ExtensionFilter(ARCHIVE, subtitleExtensions); + final var allSupportedFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions); + filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter); + filePicker.setSelectedExtensionFilter(allSupportedFilter); + final var file = filePicker.showOpenDialog(window()); + loadSubtitles(file.toPath()); + } + + @FXML + private void savePressed() { + final var filePicker = new FileChooser(); + final var archiveFilter = new FileChooser.ExtensionFilter(ARCHIVE, subtitleExtensions); + final var allSupportedFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions); + filePicker.getExtensionFilters().addAll(archiveFilter, allSupportedFilter); + filePicker.setSelectedExtensionFilter(allSupportedFilter); + final var file = filePicker.showSaveDialog(window()); + if (file != null) { + saveSubtitles(file.toPath()); + } + } + + @Override + public void selectLanguage(final Language language) { + model.setSelectedLanguage(language); + } + + @Override + public void deleteLanguage(final Language language) { + model.selectedTranslationsLanguages().remove(language); + } + + @Override + public void saveSubtitles(final Path file) { + try { + final var filename = file.getFileName().toString(); + final var extension = filename.substring(filename.lastIndexOf('.') + 1); + if (subtitleExtensions.contains(extension)) { + importerExporter.exportSubtitles(model.selectedCollection(), file); + } else { + importerExporter.exportSubtitles(model.collections().values(), file); + } + } catch (final IOException e) { + logger.error("Error saving subtitles {}", file, e); + showErrorDialog(resources.getString("subtitles.save.error.title"), MessageFormat.format(resources.getString("subtitles.save.error.label"), file)); + } + } + + @Override + public void loadSubtitles(final Path file) { + try { + final var map = importerExporter.importSubtitles(file); + map.values().forEach(this::loadCollection); + if (model.videoLanguage() == Language.AUTO) { + model.setVideoLanguage(map.keySet().stream().findFirst().orElse(Language.AUTO)); + } + } catch (final IOException | ParseException e) { + logger.error("Error loading subtitles {}", file, e); + showErrorDialog(resources.getString("subtitles.load.error.title"), MessageFormat.format(resources.getString("subtitles.load.error.label"), file)); + } + } + + private void loadCollection(final SubtitleCollection collection) { + final var observableCollection = new ObservableSubtitleCollectionImpl(collection); + model.originalCollections().put(observableCollection.language(), observableCollection); + model.collections().put(observableCollection.language(), observableCollection); + } + + private void deleteSelectedSubtitles() { + model.selectedCollection().observableSubtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems()); + } + + private void editFocusedCell() { + final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell(); + if (focusedCell != null) { + subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn()); + } + } + + @Override + public FXSubtitlesModel model() { + return model; + } + + @Override + protected Window window() { + return saveButton.getScene().getWindow(); + } +} diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesModel.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesModel.java new file mode 100644 index 0000000..328442b --- /dev/null +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSubtitlesModel.java @@ -0,0 +1,256 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.Language; +import com.github.gtache.autosubtitle.gui.SubtitlesModel; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Arrays; +import java.util.Comparator; + +/** + * FX implementation of {@link SubtitlesModel} + */ +@Singleton +public class FXSubtitlesModel implements SubtitlesModel { + + private final ObservableList availableVideoLanguages; + private final ObjectProperty videoLanguage; + private final ObservableList availableTranslationLanguages; + private final ObservableList selectedTranslationsLanguages; + private final ObjectProperty selectedLanguage; + private final ObservableMap collections; + private final ObservableMap originalCollections; + private final ObjectProperty selectedCollection; + private final ObservableList selectedSubtitles; + private final ObjectProperty selectedSubtitle; + + private final BooleanProperty canLoadSubtitles; + private final BooleanProperty canAddSubtitle; + private final BooleanProperty canResetSubtitles; + private final BooleanProperty canEditTable; + private final ReadOnlyBooleanWrapper canSaveSubtitles; + private final BooleanProperty isTranslating; + + @Inject + FXSubtitlesModel() { + this.availableVideoLanguages = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(Arrays.stream(Language.values()) + .sorted((o1, o2) -> { + if (o1 == Language.AUTO) { + return -1; + } else if (o2 == Language.AUTO) { + return 1; + } else { + return o1.englishName().compareTo(o2.englishName()); + } + }).toList())); + this.availableTranslationLanguages = FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO).toList()); + this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO); + this.selectedTranslationsLanguages = FXCollections.observableArrayList(); + this.selectedLanguage = new SimpleObjectProperty<>(Language.AUTO); + this.collections = FXCollections.observableHashMap(); + this.originalCollections = FXCollections.observableHashMap(); + this.selectedCollection = new SimpleObjectProperty<>(); + this.selectedSubtitles = FXCollections.observableArrayList(); + this.selectedSubtitle = new SimpleObjectProperty<>(); + this.canLoadSubtitles = new SimpleBooleanProperty(false); + this.canAddSubtitle = new SimpleBooleanProperty(false); + this.canResetSubtitles = new SimpleBooleanProperty(false); + this.canSaveSubtitles = new ReadOnlyBooleanWrapper(false); + this.canEditTable = new SimpleBooleanProperty(false); + this.isTranslating = new SimpleBooleanProperty(false); + + canSaveSubtitles.bind(Bindings.isNotEmpty(collections)); + collections.addListener((MapChangeListener) change -> + availableTranslationLanguages.setAll(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && !collections.containsKey(l)).sorted(Comparator.comparing(Language::englishName)).toList())); + + selectedCollection.addListener((observable, oldValue, newValue) -> { + selectedSubtitle.set(null); + if (newValue == null) { + selectedSubtitles.clear(); + selectedLanguage.set(Language.AUTO); + } else { + selectedSubtitles.setAll(newValue.subtitles()); + selectedLanguage.set(newValue.language()); + } + }); + } + + @Override + public ObservableList availableVideoLanguages() { + return availableVideoLanguages; + } + + @Override + public Language videoLanguage() { + return videoLanguage.get(); + } + + @Override + public void setVideoLanguage(final Language language) { + videoLanguage.set(language); + } + + ObjectProperty videoLanguageProperty() { + return videoLanguage; + } + + @Override + public ObservableList availableTranslationsLanguage() { + return FXCollections.unmodifiableObservableList(availableTranslationLanguages); + } + + @Override + public ObservableList selectedTranslationsLanguages() { + return selectedTranslationsLanguages; + } + + @Override + public Language selectedLanguage() { + return selectedLanguage.get(); + } + + @Override + public void setSelectedLanguage(final Language language) { + selectedLanguage.set(language); + } + + ObjectProperty selectedLanguageProperty() { + return selectedLanguage; + } + + @Override + public ObservableMap collections() { + return collections; + } + + @Override + public ObservableSubtitleCollectionImpl selectedCollection() { + return selectedCollection.get(); + } + + @Override + public void setSelectedCollection(final ObservableSubtitleCollectionImpl collection) { + selectedCollection.set(collection); + } + + ObjectProperty selectedCollectionProperty() { + return selectedCollection; + } + + @Override + public ObservableMap originalCollections() { + return originalCollections; + } + + @Override + public ObservableList selectedSubtitles() { + return selectedSubtitles; + } + + @Override + public ObservableSubtitleImpl selectedSubtitle() { + return selectedSubtitle.get(); + } + + @Override + public void setSelectedSubtitle(final ObservableSubtitleImpl subtitle) { + selectedSubtitle.set(subtitle); + } + + ObjectProperty selectedSubtitleProperty() { + return selectedSubtitle; + } + + @Override + public boolean canLoadSubtitles() { + return canLoadSubtitles.get(); + } + + @Override + public void setCanLoadSubtitles(final boolean canLoadSubtitles) { + this.canLoadSubtitles.set(canLoadSubtitles); + } + + BooleanProperty canLoadSubtitlesProperty() { + return canLoadSubtitles; + } + + @Override + public boolean canAddSubtitle() { + return canAddSubtitle.get(); + } + + @Override + public void setCanAddSubtitle(final boolean canAddSubtitle) { + this.canAddSubtitle.set(canAddSubtitle); + } + + BooleanProperty canAddSubtitleProperty() { + return canAddSubtitle; + } + + @Override + public boolean canResetSubtitles() { + return canResetSubtitles.get(); + } + + @Override + public void setCanResetSubtitles(final boolean canResetSubtitles) { + this.canResetSubtitles.set(canResetSubtitles); + } + + BooleanProperty canResetSubtitlesProperty() { + return canResetSubtitles; + } + + @Override + public boolean canSaveSubtitles() { + return canSaveSubtitles.get(); + } + + ReadOnlyBooleanProperty canSaveSubtitlesProperty() { + return canSaveSubtitles.getReadOnlyProperty(); + } + + @Override + public boolean isTranslating() { + return isTranslating.get(); + } + + @Override + public void setTranslating(final boolean translating) { + this.isTranslating.set(translating); + } + + BooleanProperty translatingProperty() { + return isTranslating; + } + + @Override + public boolean canEditTable() { + return canEditTable.get(); + } + + @Override + public void setCanEditTable(final boolean canEditTable) { + this.canEditTable.set(canEditTable); + } + + public BooleanProperty canEditTableProperty() { + return canEditTable; + } +} diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java index 30665f9..7609efc 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java @@ -5,7 +5,7 @@ import javax.inject.Singleton; import java.util.Objects; @Singleton -public class FXWorkBinder { +public class FXWorkBinder implements FXBinder { private final FXWorkModel workModel; private final FXParametersModel parametersModel; @@ -16,7 +16,8 @@ public class FXWorkBinder { this.parametersModel = Objects.requireNonNull(parametersModel); } - void createBindings() { + @Override + public void createBindings() { workModel.extractionModelProperty().bind(parametersModel.extractionModelProperty()); } } diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java index f6fc39b..b7a3a6e 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java @@ -1,49 +1,32 @@ package com.github.gtache.autosubtitle.gui.fx; -import com.github.gtache.autosubtitle.Language; -import com.github.gtache.autosubtitle.Translator; import com.github.gtache.autosubtitle.VideoConverter; import com.github.gtache.autosubtitle.VideoLoader; -import com.github.gtache.autosubtitle.gui.TimeFormatter; import com.github.gtache.autosubtitle.gui.WorkController; import com.github.gtache.autosubtitle.gui.WorkStatus; -import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; -import com.github.gtache.autosubtitle.subtitle.converter.ParseException; -import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter; import com.github.gtache.autosubtitle.subtitle.extractor.ExtractEvent; import com.github.gtache.autosubtitle.subtitle.extractor.ExtractException; import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor; import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractorListener; -import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl; import javafx.application.Platform; import javafx.beans.binding.Bindings; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; import javafx.scene.control.TextField; -import javafx.scene.control.cell.TextFieldTableCell; -import javafx.scene.input.KeyCode; import javafx.stage.FileChooser; import javafx.stage.Window; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.controlsfx.control.CheckComboBox; -import org.controlsfx.control.PrefixSelectionComboBox; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; import java.util.List; -import java.util.Map; import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -67,34 +50,18 @@ public class FXWorkController extends AbstractFXController implements WorkContro @FXML private TextField fileField; - @FXML - private Button loadSubtitlesButton; + @FXML private Button extractButton; - @FXML - private Button resetButton; + @FXML private Button exportSoftButton; @FXML private Button exportHardButton; - @FXML - private TableView subtitlesTable; - @FXML - private TableColumn startColumn; - @FXML - private TableColumn endColumn; - @FXML - private TableColumn textColumn; + @FXML private FXMediaController mediaController; - @FXML - private Button saveSubtitlesButton; - @FXML - private Button addSubtitleButton; - @FXML - private PrefixSelectionComboBox languageCombobox; - @FXML - private CheckComboBox translationsCombobox; + @FXML private Label progressLabel; @FXML @@ -107,34 +74,25 @@ public class FXWorkController extends AbstractFXController implements WorkContro private final FXWorkModel model; private final FXWorkBinder binder; private final SubtitleExtractor subtitleExtractor; - private final Map subtitleConvertersMap; private final VideoConverter videoConverter; private final VideoLoader videoLoader; - private final Translator translator; - private final TimeFormatter timeFormatter; - private final List subtitleExtensions; - @Inject FXWorkController(final FXWorkModel model, final FXWorkBinder binder, final SubtitleExtractor subtitleExtractor, - final Map subtitleConvertersMap, final VideoLoader videoLoader, - final VideoConverter videoConverter, final Translator translator, final TimeFormatter timeFormatter) { + final VideoLoader videoLoader, final VideoConverter videoConverter) { this.model = requireNonNull(model); this.binder = requireNonNull(binder); this.subtitleExtractor = requireNonNull(subtitleExtractor); - this.subtitleConvertersMap = requireNonNull(subtitleConvertersMap); this.videoConverter = requireNonNull(videoConverter); this.videoLoader = requireNonNull(videoLoader); - this.translator = requireNonNull(translator); - this.timeFormatter = requireNonNull(timeFormatter); - this.subtitleExtensions = subtitleConvertersMap.values().stream().map(c -> "*." + c.formatName()).sorted().toList(); } @FXML - private void initialize() { - bindComboboxes(); - bindButtons(); - bindTable(); + void initialize() { + extractButton.disableProperty().bind(model.canExtractProperty().not()); + exportSoftButton.disableProperty().bind(model.canExportProperty().not()); + exportHardButton.disableProperty().bind(model.canExportProperty().not()); + bindProgress(); model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> { @@ -148,91 +106,17 @@ public class FXWorkController extends AbstractFXController implements WorkContro subtitleExtractor.addListener(this); } - private void bindComboboxes() { - languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty()); - languageCombobox.setItems(model.availableVideoLanguages()); - languageCombobox.setConverter(new LanguageStringConverter()); - translationsCombobox.setConverter(new LanguageStringConverter()); - Bindings.bindContent(translationsCombobox.getItems(), model.availableTranslationsLanguage()); - Bindings.bindContent(model.translations(), translationsCombobox.getCheckModel().getCheckedItems()); - } - - private void bindButtons() { - extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - addSubtitleButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - loadSubtitlesButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - saveSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); - } - - private void bindTable() { - subtitlesTable.setItems(model.subtitles()); - subtitlesTable.setOnKeyPressed(e -> { - if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) { - editFocusedCell(); - } else if (e.getCode() == KeyCode.RIGHT || - e.getCode() == KeyCode.TAB) { - subtitlesTable.getSelectionModel().selectNext(); - e.consume(); - } else if (e.getCode() == KeyCode.LEFT) { - subtitlesTable.getSelectionModel().selectPrevious(); - e.consume(); - } else if (e.getCode() == KeyCode.DELETE) { - deleteSelectedSubtitles(); - e.consume(); - } - }); - startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter))); - startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start())); - startColumn.setOnEditCommit(e -> { - final var subtitle = e.getRowValue(); - subtitle.setStart(e.getNewValue()); - subtitlesTable.refresh(); - subtitlesTable.requestFocus(); - }); - endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter))); - endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end())); - endColumn.setOnEditCommit(e -> { - final var subtitle = e.getRowValue(); - subtitle.setEnd(e.getNewValue()); - subtitlesTable.refresh(); - subtitlesTable.requestFocus(); - }); - textColumn.setCellFactory(TextFieldTableCell.forTableColumn()); - textColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue() == null ? null : param.getValue().content())); - textColumn.setOnEditCommit(e -> { - final var subtitle = e.getRowValue(); - subtitle.setContent(e.getNewValue()); - subtitlesTable.refresh(); - subtitlesTable.requestFocus(); - }); - - subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue)); - } - private void bindProgress() { progressLabel.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("work.status." + model.status().name().toLowerCase() + ".label"), model.statusProperty())); - progressLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)); - progressBar.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)); - progressDetailLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)); - progressLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)); - progressBar.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)); - progressDetailLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)); + progressLabel.visibleProperty().bind(model.isProgressVisibleProperty()); + progressBar.visibleProperty().bind(model.isProgressVisibleProperty()); + progressDetailLabel.visibleProperty().bind(model.isProgressVisibleProperty()); + progressLabel.managedProperty().bind(model.isProgressVisibleProperty()); + progressBar.managedProperty().bind(model.isProgressVisibleProperty()); + progressDetailLabel.managedProperty().bind(model.isProgressVisibleProperty()); progressBar.progressProperty().bindBidirectional(model.progressProperty()); } - private void deleteSelectedSubtitles() { - model.subtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems()); - } - - private void editFocusedCell() { - final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell(); - if (focusedCell != null) { - subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn()); - } - } @FXML private void fileButtonPressed() { @@ -254,17 +138,17 @@ public class FXWorkController extends AbstractFXController implements WorkContro } } - private SubtitleCollection extractAsync() { + private SubtitleCollection extractAsync() { try { - return subtitleExtractor.extract(model.video(), model.videoLanguage(), model.extractionModel()); + return subtitleExtractor.extract(model.video(), model.videoLanguageProperty().get(), model.extractionModel()); } catch (final ExtractException e) { throw new CompletionException(e); } } - private void manageExtractResult(final SubtitleCollection newCollection, final Throwable t) { + private void manageExtractResult(final SubtitleCollection newCollection, final Throwable t) { if (t == null) { - loadCollection(newCollection); + model.setExtractedCollection(newCollection); } else { logger.error("Error extracting subtitles", t); showErrorDialog(resources.getString("work.extract.error.title"), MessageFormat.format(resources.getString("work.extract.error.label"), t.getMessage())); @@ -284,45 +168,6 @@ public class FXWorkController extends AbstractFXController implements WorkContro } } - @Override - public void saveSubtitles(final Path file) { - final var fileName = file.getFileName().toString(); - final var converter = subtitleConvertersMap.get(fileName.substring(fileName.lastIndexOf('.') + 1)); - if (converter == null) { - logger.warn("No converter for {}", file); - showErrorDialog(resources.getString("work.save.subtitles.missing.converter.title"), MessageFormat.format(resources.getString("work.save.subtitles.missing.converter.label"), file.getFileName())); - } else { - final var string = converter.format(model.subtitleCollection()); - try { - Files.writeString(file, string); - } catch (final IOException e) { - logger.error("Error saving subtitles {}", file, e); - showErrorDialog(resources.getString("work.save.subtitles.error.title"), MessageFormat.format(resources.getString("work.save.subtitles.error.label"), e.getMessage())); - } - } - } - - @Override - public void loadSubtitles(final Path file) { - final var fileName = file.getFileName().toString(); - final var parser = subtitleConvertersMap.get(fileName.substring(fileName.lastIndexOf('.') + 1)); - if (parser != null) { - try { - final var collection = parser.parse(file); - loadCollection(collection); - } catch (final ParseException e) { - logger.error("Error loading subtitles {}", file, e); - showErrorDialog(resources.getString("work.load.subtitles.error.title"), MessageFormat.format(resources.getString("work.load.subtitles.error.label"), e.getMessage())); - } - } - } - - private void loadCollection(final SubtitleCollection collection) { - model.subtitles().setAll(collection.subtitles().stream().map(ObservableSubtitleImpl::new).toList()); - model.originalSubtitles().clear(); - model.originalSubtitles().addAll(collection.subtitles().stream().map(ObservableSubtitleImpl::new).toList()); - model.videoLanguageProperty().set(collection.language()); - } @FXML private void exportSoftPressed() { @@ -332,27 +177,20 @@ public class FXWorkController extends AbstractFXController implements WorkContro filePicker.setSelectedExtensionFilter(extensionFilter); final var file = filePicker.showSaveDialog(window()); if (file != null) { - final var baseCollection = model.subtitleCollection(); - final var translations = model.translations(); - model.setStatus(WorkStatus.TRANSLATING); - CompletableFuture.supplyAsync(() -> Stream.concat(Stream.of(baseCollection), translations.stream().map(l -> translator.translate(baseCollection, l))).toList()) - .thenApplyAsync(c -> { - model.setStatus(WorkStatus.EXPORTING); - return c; - }, Platform::runLater) - .thenAcceptAsync(collections -> { - try { - videoConverter.addSoftSubtitles(model.video(), collections, file.toPath()); - } catch (final IOException e) { - throw new CompletionException(e); - } - }).whenCompleteAsync((v, t) -> { - if (t != null) { - logger.error("Error exporting subtitles", t); - showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage())); - } - model.setStatus(WorkStatus.IDLE); - }, Platform::runLater); + model.setStatus(WorkStatus.EXPORTING); + CompletableFuture.runAsync(() -> { + try { + videoConverter.addSoftSubtitles(model.video(), model.collections().values(), file.toPath()); + } catch (final IOException e) { + throw new CompletionException(e); + } + }).whenCompleteAsync((v, t) -> { + if (t != null) { + logger.error("Error exporting subtitles", t); + showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage())); + } //else show info dialog + model.setStatus(WorkStatus.IDLE); + }, Platform::runLater); } } @@ -366,7 +204,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro if (file != null) { CompletableFuture.runAsync(() -> { try { - videoConverter.addHardSubtitles(model.video(), model.subtitleCollection(), file.toPath()); + videoConverter.addHardSubtitles(model.video(), model.collections().get(model.videoLanguageProperty().get()), file.toPath()); } catch (final IOException e) { throw new CompletionException(e); } @@ -374,7 +212,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro if (t != null) { logger.error("Error exporting subtitles", t); showErrorDialog(resources.getString("work.export.error.title"), MessageFormat.format(resources.getString("work.export.error.label"), t.getMessage())); - } + } //else show info dialog model.setStatus(WorkStatus.IDLE); }, Platform::runLater); } @@ -386,6 +224,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro return model; } + @Override public Window window() { return fileField.getScene().getWindow(); } @@ -395,16 +234,6 @@ public class FXWorkController extends AbstractFXController implements WorkContro extractSubtitles(); } - @FXML - private void resetButtonPressed() { - model.subtitles().setAll(model.originalSubtitles()); - } - - @FXML - private void addSubtitlePressed() { - model.subtitles().add(new ObservableSubtitleImpl("Enter text here...")); - } - @Override public void listen(final ExtractEvent event) { Platform.runLater(() -> { @@ -413,25 +242,5 @@ public class FXWorkController extends AbstractFXController implements WorkContro }); } - @FXML - private void loadSubtitlesPressed() { - final var filePicker = new FileChooser(); - final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions); - filePicker.getExtensionFilters().add(extensionFilter); - filePicker.setSelectedExtensionFilter(extensionFilter); - final var file = filePicker.showOpenDialog(window()); - loadSubtitles(file.toPath()); - } - @FXML - private void saveSubtitlesPressed() { - final var filePicker = new FileChooser(); - final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions); - filePicker.getExtensionFilters().add(extensionFilter); - filePicker.setSelectedExtensionFilter(extensionFilter); - final var file = filePicker.showSaveDialog(window()); - if (file != null) { - saveSubtitles(file.toPath()); - } - } } diff --git a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java index f9adc79..8a05892 100644 --- a/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java +++ b/gui/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java @@ -7,26 +7,22 @@ import com.github.gtache.autosubtitle.gui.WorkStatus; import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel; -import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl; -import javafx.beans.binding.Bindings; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleCollectionImpl; +import com.github.gtache.autosubtitle.subtitle.gui.fx.ObservableSubtitleImpl; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; /** * FX implementation of {@link WorkModel} @@ -35,51 +31,35 @@ import java.util.stream.Collectors; public class FXWorkModel implements WorkModel { private final ObjectProperty