diff --git a/.gitignore b/.gitignore index 5e526d0..b40f052 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,6 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +tools \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 412d5ee..0e3521c 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -3,6 +3,8 @@ + + diff --git a/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java b/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java index 68bd2cb..701cdf4 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java @@ -7,5 +7,10 @@ public interface AudioInfo { /** * @return The audio extension (mp3, etc.) */ - String videoFormat(); + String audioFormat(); + + /** + * @return The audio duration in milliseconds + */ + long duration(); } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/Language.java b/api/src/main/java/com/github/gtache/autosubtitle/Language.java new file mode 100644 index 0000000..a9da8ba --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/Language.java @@ -0,0 +1,97 @@ +package com.github.gtache.autosubtitle; + +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +/** + * A list of languages //TODO add more or use Locale if possible? + */ +public enum Language { + AR("arabic", "ar", "ara"), + BE("belarusian", "be", "bel"), + BG("bulgarian", "bg", "bul"), + CS("czech", "cs", "cze"), + DA("danish", "da", "dan"), + DE("german", "de", "deu", "ger"), + EL("greek", "el", "gre"), + EN("english", "en", "eng"), + ES("spanish", "es", "spa"), + FA("persian", "fa", "per"), + FI("finnish", "fi", "fin"), + FR("french", "fr", "fra", "fre"), + HE("hebrew", "he", "heb"), + HR("croatian", "hr", "hrv"), + ID("indonesian", "id", "ind"), + IT("italian", "it", "ita", "gre"), + JA("japanese", "ja", "jpn"), + KO("korean", "ko", "kor"), + LA("latin", "la", "lat"), + LB("luxembourgish", "lb", "ltz"), + LO("lao", "lo", "lao"), + LT("lithuanian", "lt", "lit"), + MT("maltese", "mt", "mlt"), + MY("myanmar", "my", "mya"), + NL("dutch", "nl", "nld"), + NO("norwegian", "no", "nor"), + PL("polish", "pl", "pol"), + PT("portuguese", "pt", "por"), + RO("romanian", "ro", "ron"), + RU("russian", "ru", "rus"), + SK("slovak", "sk", "slo"), + SL("slovenian", "sl", "slv"), + SV("swedish", "sv", "swe"), + TH("thai", "th", "tha"), + TR("turkish", "tr", "tur"), + UK("ukrainian", "uk", "ukr"), + VI("vietnamese", "vi", "vie"), + ZH("chinese", "zh", "zho", "chi"), + AUTO("auto", "auto", "auto"); + + private static final Map STRING_LANGUAGE_MAP; + + static { + final Map map = new java.util.HashMap<>(); + for (final var language : Language.values()) { + map.put(language.name().toLowerCase(), language); + map.put(language.iso2, language); + map.put(language.iso3, language); + language.aliases.forEach(s -> map.put(s, language)); + } + STRING_LANGUAGE_MAP = map; + } + + private final String englishName; + private final String iso2; + private final String iso3; + private final Set aliases; + + Language(final String englishName, final String iso2, final String iso3, final String... aliases) { + this.englishName = requireNonNull(englishName); + this.iso2 = requireNonNull(iso2); + this.iso3 = requireNonNull(iso3); + this.aliases = Set.of(aliases); + } + + public String englishName() { + return englishName; + } + + public String iso2() { + return iso2; + } + + public String iso3() { + return iso3; + } + + public static Language getLanguage(final String name) { + return STRING_LANGUAGE_MAP.get(name.toLowerCase()); + } + + public static Language getDefault() { + return STRING_LANGUAGE_MAP.getOrDefault(Locale.getDefault().getLanguage(), EN); + } +} 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 0cee55c..663d0da 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/Translator.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/Translator.java @@ -3,88 +3,86 @@ package com.github.gtache.autosubtitle; import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; -import java.util.Locale; - /** * Translates texts and subtitles */ public interface Translator { /** - * Guesses the locale of the given text + * Guesses the language of the given text * * @param text The text - * @return The guessed locale + * @return The guessed language */ - Locale getLocale(final String text); + Language getLanguage(final String text); /** - * Guesses the locale of the given subtitle + * Guesses the language of the given subtitle * * @param subtitle The subtitle - * @return The guessed locale + * @return The guessed language */ - default Locale getLocale(final Subtitle subtitle) { - return getLocale(subtitle.content()); + default Language getLanguage(final Subtitle subtitle) { + return getLanguage(subtitle.content()); } /** - * Translates the given text to the given locale + * Translates the given text to the given language * * @param text The text to translate - * @param to The target locale + * @param to The target language * @return The translated text */ - String translate(String text, Locale to); + String translate(String text, Language to); /** - * Translates the given text to the given locale + * Translates the given text to the given language * * @param text The text to translate - * @param to The target locale + * @param to The target language * @return The translated text */ default String translate(final String text, final String to) { - return translate(text, Locale.forLanguageTag(to)); + return translate(text, Language.getLanguage(to)); } /** - * Translates the given subtitle to the given locale + * Translates the given subtitle to the given language * * @param subtitle The subtitle to translate - * @param to The target locale + * @param to The target language * @return The translated subtitle */ - Subtitle translate(Subtitle subtitle, Locale to); + Subtitle translate(Subtitle subtitle, Language to); /** - * Translates the given subtitle to the given locale + * Translates the given subtitle to the given language * * @param subtitle The subtitle to translate - * @param to The target locale + * @param to The target language * @return The translated subtitle */ default Subtitle translate(final Subtitle subtitle, final String to) { - return translate(subtitle, Locale.forLanguageTag(to)); + return translate(subtitle, Language.getLanguage(to)); } /** - * Translates the given subtitles collection to the given locale + * Translates the given subtitles collection to the given language * * @param collection The subtitles collection to translate - * @param to The target locale + * @param to The target language * @return The translated subtitles collection */ default SubtitleCollection translate(final SubtitleCollection collection, final String to) { - return translate(collection, Locale.forLanguageTag(to)); + return translate(collection, Language.getLanguage(to)); } /** - * Translates the given subtitles collection to the given locale + * Translates the given subtitles collection to the given language * * @param collection The subtitles collection to translate - * @param to The target locale + * @param to The target language * @return The translated subtitles collection */ - SubtitleCollection translate(SubtitleCollection collection, Locale to); + SubtitleCollection translate(SubtitleCollection collection, Language to); } 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 new file mode 100644 index 0000000..0e58649 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessListener.java @@ -0,0 +1,31 @@ +package com.github.gtache.autosubtitle.process; + +import java.io.IOException; +import java.time.Duration; + +/** + * Listens to a process + */ +public interface ProcessListener { + + /** + * @return The process + */ + Process process(); + + /** + * Waits for the next output of the process (or its end). Note that the process may become stuck if the output is not read. + * + * @return The next line of the process output, or null if the process has ended + * @throws IOException if an error occurs + */ + String readLine() throws IOException; + + /** + * Waits for the process to finish + * + * @param duration The maximum time to wait + * @return The process result + */ + 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 3321244..cfbddca 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 @@ -28,4 +28,44 @@ public interface ProcessRunner { * @throws IOException if something goes wrong */ ProcessResult run(final List args) throws IOException; + + /** + * Starts a process + * + * @param args the command + * @return The process + * @throws IOException if something goes wrong + */ + default Process start(final String... args) throws IOException { + return start(Arrays.asList(args)); + } + + /** + * Starts a process + * + * @param args the command + * @return The process + * @throws IOException if something goes wrong + */ + Process start(final List args) throws IOException; + + /** + * Starts a process + * + * @param args the command + * @return An object allowing to listen to the process + * @throws IOException if something goes wrong + */ + default ProcessListener startListen(final String... args) throws IOException { + return startListen(Arrays.asList(args)); + } + + /** + * Starts a process + * + * @param args the command + * @return An object allowing to listen to the process + * @throws IOException if something goes wrong + */ + ProcessListener startListen(final List args) throws IOException; } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupAction.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupAction.java new file mode 100644 index 0000000..29e4e0c --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupAction.java @@ -0,0 +1,8 @@ +package com.github.gtache.autosubtitle.setup; + +/** + * Represents a setup action + */ +public enum SetupAction { + CHECK, DOWNLOAD, INSTALL, UNINSTALL, UPDATE, DELETE +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupEvent.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupEvent.java new file mode 100644 index 0000000..ff3fd79 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupEvent.java @@ -0,0 +1,27 @@ +package com.github.gtache.autosubtitle.setup; + +/** + * Events that can be triggered by {@link SetupManager} + */ +public interface SetupEvent { + + /** + * @return the action that triggered the event + */ + SetupAction action(); + + /** + * @return the target of the action + */ + String target(); + + /** + * @return the progress of the setup + */ + double progress(); + + /** + * @return the setup manager that triggered the event + */ + SetupManager setupManager(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupListener.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupListener.java new file mode 100644 index 0000000..21fcf80 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupListener.java @@ -0,0 +1,21 @@ +package com.github.gtache.autosubtitle.setup; + +/** + * Listens on {@link SetupManager}'s {@link SetupEvent}s + */ +public interface SetupListener { + + /** + * Triggered when an action starts + * + * @param event the event + */ + void onActionStart(SetupEvent event); + + /** + * Triggered when an action ends + * + * @param event the event + */ + void onActionEnd(SetupEvent event); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java index b95c0c0..9d91e4b 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java @@ -17,11 +17,9 @@ public interface SetupManager { /** * @return whether the component is installed - * @throws SetupException if an error occurred during the check + * @throws SetupException if an error occurred */ - default boolean isInstalled() throws SetupException { - return status().isInstalled(); - } + boolean isInstalled() throws SetupException; /** * Installs the component @@ -42,10 +40,7 @@ public interface SetupManager { * * @throws SetupException if an error occurred during the reinstallation */ - default void reinstall() throws SetupException { - uninstall(); - install(); - } + void reinstall() throws SetupException; /** * Checks if an update is available for the component @@ -53,9 +48,7 @@ public interface SetupManager { * @return whether an update is available * @throws SetupException if an error occurred during the check */ - default boolean isUpdateAvailable() throws SetupException { - return status() == SetupStatus.UPDATE_AVAILABLE; - } + boolean isUpdateAvailable() throws SetupException; /** * Updates the component @@ -63,4 +56,23 @@ public interface SetupManager { * @throws SetupException if an error occurred during the update */ void update() throws SetupException; + + /** + * Adds a listener + * + * @param listener the listener + */ + void addListener(SetupListener listener); + + /** + * Removes a listener + * + * @param listener the listener + */ + void removeListener(SetupListener listener); + + /** + * Removes all listeners + */ + void removeListeners(); } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java index 15fbbcb..cc0e2e4 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java @@ -4,9 +4,9 @@ package com.github.gtache.autosubtitle.setup; * The status of a setup */ public enum SetupStatus { - ERRORED, NOT_INSTALLED, INSTALLED, UPDATE_AVAILABLE; + ERRORED, NOT_INSTALLED, SYSTEM_INSTALLED, BUNDLE_INSTALLED, UPDATE_AVAILABLE; public boolean isInstalled() { - return this == INSTALLED || this == UPDATE_AVAILABLE; + return this == SYSTEM_INSTALLED || this == BUNDLE_INSTALLED || this == UPDATE_AVAILABLE; } } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractEvent.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractEvent.java new file mode 100644 index 0000000..1662a8b --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractEvent.java @@ -0,0 +1,17 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * Events that can be triggered by {@link SubtitleExtractor} + */ +public interface ExtractEvent { + + /** + * @return the message + */ + String message(); + + /** + * @return the progress of the setup + */ + double progress(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractException.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractException.java new file mode 100644 index 0000000..b641e1b --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractException.java @@ -0,0 +1,19 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * Exception thrown when an error occurs during subtitle extraction + */ +public class ExtractException extends Exception { + + public ExtractException(final String message) { + super(message); + } + + public ExtractException(final String message, final Throwable cause) { + super(message, cause); + } + + public ExtractException(final Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractionModel.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractionModel.java new file mode 100644 index 0000000..dace168 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractionModel.java @@ -0,0 +1,11 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * An extraction model + */ +public interface ExtractionModel { + /** + * @return the name of the model + */ + String name(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractionModelProvider.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractionModelProvider.java new file mode 100644 index 0000000..a84df69 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/ExtractionModelProvider.java @@ -0,0 +1,25 @@ +package com.github.gtache.autosubtitle.subtitle; + +import java.util.List; + +/** + * Provider of {@link ExtractionModel} + */ +public interface ExtractionModelProvider { + + /** + * @return the list of all available models + */ + List getAvailableExtractionModels(); + + /** + * @return the default model + */ + ExtractionModel getDefaultExtractionModel(); + + /** + * @param name the name of the model + * @return the model with the specified name + */ + ExtractionModel getExtractionModel(final String name); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/OutputFormat.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/OutputFormat.java new file mode 100644 index 0000000..89f7f2c --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/OutputFormat.java @@ -0,0 +1,8 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * The possible subtitles output formats + */ +public enum OutputFormat { + SRT, ASS +} 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 0b0a053..c385b36 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 @@ -1,7 +1,8 @@ package com.github.gtache.autosubtitle.subtitle; +import com.github.gtache.autosubtitle.Language; + import java.util.Collection; -import java.util.Locale; /** * Represents a collection of {@link Subtitle} @@ -14,7 +15,7 @@ public interface SubtitleCollection { Collection subtitles(); /** - * @return The locale of the subtitles + * @return The language of the subtitles */ - Locale locale(); + Language language(); } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java index 08c006b..f79bcd0 100644 --- a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java @@ -1,16 +1,76 @@ package com.github.gtache.autosubtitle.subtitle; import com.github.gtache.autosubtitle.Audio; +import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.Video; -import java.util.Collection; - /** * Extracts subtitles from a video or audio */ public interface SubtitleExtractor { - Collection extract(final Video in); + /** + * Adds a listener + * + * @param listener The listener + */ + void addListener(SubtitleExtractorListener listener); - Collection extract(final Audio in); + /** + * Removes a listener + * + * @param listener The listener + */ + void removeListener(SubtitleExtractorListener listener); + + /** + * Removes all listeners + */ + void removeListeners(); + + /** + * Extracts the subtitles from a video + * + * @param video The video + * @param model The model to use + * @return The extracted subtitle collection + * @throws ExtractException If an error occurs + */ + default SubtitleCollection extract(final Video video, final ExtractionModel model) throws ExtractException { + return extract(video, Language.AUTO, model); + } + + /** + * Extracts the subtitles from a video + * + * @param video The video + * @param language The language of the video + * @param model The model to use + * @return The extracted subtitle collection + * @throws ExtractException If an error occurs + */ + SubtitleCollection extract(final Video video, final Language language, final ExtractionModel model) throws ExtractException; + + /** + * Extracts the subtitles from an audio + * + * @param audio The audio + * @param model The model to use + * @return The extracted subtitle collection + * @throws ExtractException If an error occurs + */ + default SubtitleCollection extract(final Audio audio, final ExtractionModel model) throws ExtractException { + return extract(audio, Language.AUTO, model); + } + + /** + * Extracts the subtitles from an audio + * + * @param audio The audio + * @param language The language of the audio + * @param model The model to use + * @return The extracted subtitle collection + * @throws ExtractException If an error occurs + */ + SubtitleCollection extract(final Audio audio, final Language language, final ExtractionModel model) throws ExtractException; } diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractorListener.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractorListener.java new file mode 100644 index 0000000..e77d5cf --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractorListener.java @@ -0,0 +1,15 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * Listener for {@link SubtitleExtractor} + */ +@FunctionalInterface +public interface SubtitleExtractorListener { + + /** + * Called when an event is triggered + * + * @param event The event + */ + void listen(final ExtractEvent event); +} diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 0000000..70e0d72 --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.github.gtache.autosubtitle + autosubtitle + 1.0-SNAPSHOT + + + autosubtitle-cli + + + + com.github.gtache.autosubtitle + autosubtitle-deepl + + + com.github.gtache.autosubtitle + autosubtitle-ffmpeg + + + com.github.gtache.autosubtitle + autosubtitle-whisper + + + org.apache.logging.log4j + log4j-core + + + com.google.dagger + dagger + + + info.picocli + picocli + + + + \ No newline at end of file diff --git a/cli/src/main/java/com/github/gtache/autosubtitle/cli/Cli.java b/cli/src/main/java/com/github/gtache/autosubtitle/cli/Cli.java new file mode 100644 index 0000000..0439e22 --- /dev/null +++ b/cli/src/main/java/com/github/gtache/autosubtitle/cli/Cli.java @@ -0,0 +1,61 @@ +package com.github.gtache.autosubtitle.cli; + + +import com.github.gtache.autosubtitle.modules.cli.DaggerCliComponent; +import picocli.CommandLine; + +import java.util.Set; + +@CommandLine.Command(name = "autosubtitle", mixinStandardHelpOptions = true, version = "autosubtitle 1.0-SNAPSHOT", description = "CLI for auto-subtitle") +public final class Cli implements Runnable { + + @CommandLine.Option(names = {"-b", "--burn"}, description = "Burn the subtitles. Otherwise, adds them to the video", defaultValue = "false") + private boolean burn; + @CommandLine.Option(names = {"-e", "--extractor"}, description = "The subtitle extractor to use [whisper]", defaultValue = "whisper") + private String extractor; + @CommandLine.Option(names = {"-i", "--input"}, description = "The input file", required = true) + private String input; + @CommandLine.Option(names = {"-l", "--loader"}, description = "The video loader to use [ffprobe]", defaultValue = "ffprobe") + private String loader; + @CommandLine.Option(names = {"-o", "--output"}, description = "The output file", required = true) + private String output; + @CommandLine.Option(names = {"-s", "--subtitle-converter"}, description = "The subtitle converter to use [srt|ass]", defaultValue = "srt") + private String subtitleConverter; + @CommandLine.Option(names = {"-c", "--video-converter"}, description = "The video converter to use [ffmpeg]", defaultValue = "ffmpeg") + private String videoConverter; + @CommandLine.Option(names = {"--translations"}, description = "The list of translations to create. Ignored if burn is specified", split = ",", arity = "0..*") + private Set translations; + @CommandLine.Option(names = {"-t", "--translator"}, description = "The translator to use [deepl]. Ignored if burn is specified", defaultValue = "deepl") + private String translator; + @CommandLine.Option(names = {"-w", "--wait"}, description = "Allow modifying subtitle files before creating output", defaultValue = "false") + private boolean wait; + + private Cli() { + + } + + @Override + public void run() { + if (!extractor.equals("whisper")) { + throw new IllegalArgumentException("Unknown extractor : " + extractor); + } + if (!loader.equals("ffprobe")) { + throw new IllegalArgumentException("Unknown loader : " + loader); + } + if (!subtitleConverter.equals("srt")) { + throw new IllegalArgumentException("Unknown subtitle converter : " + subtitleConverter); + } + if (!videoConverter.equals("ffmpeg")) { + throw new IllegalArgumentException("Unknown video converter : " + videoConverter); + } + if (!translator.equals("deepl")) { + throw new IllegalArgumentException("Unknown translator : " + translator); + } + + final var component = DaggerCliComponent.create(); + } + + public static void main(final String[] args) { + System.exit(new CommandLine(new Cli()).execute(args)); + } +} 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 new file mode 100644 index 0000000..2fa888d --- /dev/null +++ b/cli/src/main/java/com/github/gtache/autosubtitle/modules/cli/CliComponent.java @@ -0,0 +1,18 @@ +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.subtitles.impl.ConverterModule; +import com.github.gtache.autosubtitle.modules.whisper.WhisperModule; +import com.github.gtache.autosubtitle.subtitle.SubtitleConverter; +import dagger.Component; + +import javax.inject.Singleton; + +@Component(modules = {ConverterModule.class, CoreModule.class, DeepLModule.class, FFmpegModule.class, WhisperModule.class}) +@Singleton +public interface CliComponent { + + SubtitleConverter getSubtitleConverter(); +} diff --git a/cli/src/main/java/module-info.java b/cli/src/main/java/module-info.java new file mode 100644 index 0000000..74a5d6b --- /dev/null +++ b/cli/src/main/java/module-info.java @@ -0,0 +1,9 @@ +/** + * CLI module for autosubtitle + */ +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 info.picocli; +} \ No newline at end of file diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/Architecture.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/Architecture.java index c41accb..8a26262 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/impl/Architecture.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/Architecture.java @@ -4,7 +4,17 @@ package com.github.gtache.autosubtitle.impl; * The list of possible operating systems */ public enum Architecture { - I386, I486, I586, I686, PPC, POWERPC, X86, X86_32, X86_64, AMD64, ARM, ARM32, ARM64, AARCH64, UNKNOWN; + //32 bit + I386, I486, I586, I686, X86_32, + //PowerPC + PPC, POWERPC, + //64 bit + X86, X86_64, AMD64, + //ARM 32 bit + ARM32, ARM, ARMV1, ARMV2, ARMV3, ARMV4, ARMV5, ARMV6, ARMV7, AARCH32, + //ARM 64 bit + ARM64, ARMV8, ARMV9, AARCH64, + UNKNOWN; public static Architecture getArchitecture(final String name) { try { @@ -13,4 +23,12 @@ public enum Architecture { return UNKNOWN; } } + + public boolean isAMD64() { + return this == X86 || this == X86_64 || this == AMD64; + } + + public boolean isARM64() { + return this == ARM64 || this == ARMV8 || this == ARMV9 || this == AARCH64; + } } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java index 704af00..61bd0e4 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java @@ -7,14 +7,12 @@ import java.util.Objects; /** * Implementation of {@link AudioInfo} */ -public record AudioInfoImpl(String audioFormat) implements AudioInfo { +public record AudioInfoImpl(String audioFormat, long duration) implements AudioInfo { public AudioInfoImpl { Objects.requireNonNull(audioFormat); - } - - @Override - public String videoFormat() { - return audioFormat; + if (duration < 0) { + throw new IllegalArgumentException("Duration must be positive"); + } } } diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/SubtitleExtractorSetup.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SubtitleExtractorSetup.java similarity index 87% rename from core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/SubtitleExtractorSetup.java rename to core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SubtitleExtractorSetup.java index bff9369..02d18b9 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/SubtitleExtractorSetup.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/SubtitleExtractorSetup.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.setup.modules.impl; +package com.github.gtache.autosubtitle.modules.setup.impl; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/TranslatorSetup.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/TranslatorSetup.java similarity index 87% rename from core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/TranslatorSetup.java rename to core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/TranslatorSetup.java index 8a45dc6..026bd9f 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/TranslatorSetup.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/TranslatorSetup.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.setup.modules.impl; +package com.github.gtache.autosubtitle.modules.setup.impl; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/VideoConverterSetup.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/VideoConverterSetup.java similarity index 87% rename from core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/VideoConverterSetup.java rename to core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/VideoConverterSetup.java index 58f65e3..448ab49 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/VideoConverterSetup.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/setup/impl/VideoConverterSetup.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.setup.modules.impl; +package com.github.gtache.autosubtitle.modules.setup.impl; import javax.inject.Qualifier; import java.lang.annotation.Documented; diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/modules/impl/ConverterModule.java b/core/src/main/java/com/github/gtache/autosubtitle/modules/subtitles/impl/ConverterModule.java similarity index 77% rename from core/src/main/java/com/github/gtache/autosubtitle/subtitle/modules/impl/ConverterModule.java rename to core/src/main/java/com/github/gtache/autosubtitle/modules/subtitles/impl/ConverterModule.java index 4b913c0..0811b25 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/modules/impl/ConverterModule.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/modules/subtitles/impl/ConverterModule.java @@ -1,12 +1,10 @@ -package com.github.gtache.autosubtitle.subtitle.modules.impl; +package com.github.gtache.autosubtitle.modules.subtitles.impl; import com.github.gtache.autosubtitle.subtitle.SubtitleConverter; import com.github.gtache.autosubtitle.subtitle.impl.SRTSubtitleConverter; import dagger.Binds; import dagger.Module; -import javax.inject.Singleton; - /** * Dagger module for subtitle converter */ @@ -14,6 +12,5 @@ import javax.inject.Singleton; public interface ConverterModule { @Binds - @Singleton SubtitleConverter bindsSubtitleConverter(final SRTSubtitleConverter converter); } 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 dd87c23..fd5a461 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 @@ -1,19 +1,15 @@ package com.github.gtache.autosubtitle.process.impl; +import com.github.gtache.autosubtitle.process.ProcessListener; import com.github.gtache.autosubtitle.process.ProcessResult; import com.github.gtache.autosubtitle.process.ProcessRunner; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.io.BufferedInputStream; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; /** * Base implementation of {@link ProcessRunner} @@ -24,29 +20,30 @@ public abstract class AbstractProcessRunner implements ProcessRunner { @Override public ProcessResult run(final List args) throws IOException { - final var builder = new ProcessBuilder(args); - builder.redirectErrorStream(true); - final var process = builder.start(); - final var readFuture = CompletableFuture.supplyAsync(() -> { - final var output = new ArrayList(); - try (final var in = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream()), StandardCharsets.UTF_8))) { - var line = in.readLine(); + final var listener = startListen(args); + CompletableFuture.runAsync(() -> { + try { + var line = listener.readLine(); while (line != null) { - output.add(line); - line = in.readLine(); + line = listener.readLine(); } } catch (final IOException e) { logger.error("Error listening to process output of {}", args, e); } - return output; }); - try { - process.waitFor(1, TimeUnit.HOURS); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - process.destroy(); - } - final var output = readFuture.join(); - return new ProcessResultImpl(process.exitValue(), output); + return listener.join(Duration.ofHours(1)); + } + + @Override + public Process start(final List args) throws IOException { + final var builder = new ProcessBuilder(args); + builder.redirectErrorStream(true); + return builder.start(); + } + + @Override + public ProcessListener startListen(final List args) throws IOException { + final var process = start(args); + return new ProcessListenerImpl(process); } } 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 new file mode 100644 index 0000000..976fd18 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessListenerImpl.java @@ -0,0 +1,65 @@ +package com.github.gtache.autosubtitle.process.impl; + +import com.github.gtache.autosubtitle.process.ProcessListener; +import com.github.gtache.autosubtitle.process.ProcessResult; + +import java.io.BufferedInputStream; +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.TimeUnit; + +/** + * Implementation of {@link ProcessListener} + */ +public class ProcessListenerImpl implements ProcessListener { + + private final Process process; + private final BufferedReader reader; + private final List output; + + /** + * Instantiates the listener + * + * @param process The process to listen to + */ + public ProcessListenerImpl(final Process process) { + this.process = Objects.requireNonNull(process); + this.reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream()), StandardCharsets.UTF_8)); + this.output = new ArrayList<>(); + } + + @Override + public Process process() { + return process; + } + + @Override + public String readLine() throws IOException { + final var line = reader.readLine(); + if (line != null) { + output.add(line); + } + return line; + } + + @Override + public ProcessResult join(final Duration duration) throws IOException { + try { + process.waitFor(duration.getSeconds(), TimeUnit.SECONDS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + process.destroy(); + } + if (process.isAlive()) { + process.destroyForcibly(); + } + reader.close(); + return new ProcessResultImpl(process.exitValue(), output); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/impl/AbstractSetupManager.java b/core/src/main/java/com/github/gtache/autosubtitle/setup/impl/AbstractSetupManager.java new file mode 100644 index 0000000..2c887a7 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/setup/impl/AbstractSetupManager.java @@ -0,0 +1,166 @@ +package com.github.gtache.autosubtitle.setup.impl; + +import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner; +import com.github.gtache.autosubtitle.setup.SetupAction; +import com.github.gtache.autosubtitle.setup.SetupEvent; +import com.github.gtache.autosubtitle.setup.SetupException; +import com.github.gtache.autosubtitle.setup.SetupListener; +import com.github.gtache.autosubtitle.setup.SetupManager; +import com.github.gtache.autosubtitle.setup.SetupStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Base class for all {@link SetupManager} implementations + */ +public abstract class AbstractSetupManager extends AbstractProcessRunner implements SetupManager { + + private static final Logger logger = LogManager.getLogger(AbstractSetupManager.class); + private final Set listeners; + + /** + * Instantiates the manager + */ + protected AbstractSetupManager() { + this.listeners = new HashSet<>(); + } + + @Override + public void addListener(final SetupListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(final SetupListener listener) { + listeners.remove(listener); + } + + @Override + public void removeListeners() { + listeners.clear(); + } + + @Override + public void reinstall() throws SetupException { + sendStartEvent(SetupAction.UNINSTALL, name(), 0); + uninstall(); + sendEndEvent(SetupAction.UNINSTALL, name(), -1); + sendStartEvent(SetupAction.INSTALL, name(), -1); + install(); + sendEndEvent(SetupAction.INSTALL, name(), 1); + } + + @Override + public boolean isInstalled() { + return status() == SetupStatus.SYSTEM_INSTALLED || status() == SetupStatus.BUNDLE_INSTALLED || status() == SetupStatus.UPDATE_AVAILABLE; + } + + @Override + public boolean isUpdateAvailable() { + return status() == SetupStatus.UPDATE_AVAILABLE; + } + + @Override + public SetupStatus status() { + sendStartEvent(SetupAction.CHECK, name(), 0); + try { + final var status = getStatus(); + sendEndEvent(SetupAction.CHECK, name(), 1); + return status; + } catch (final SetupException e) { + logger.error("Error getting status of {}", name(), e); + sendEndEvent(SetupAction.CHECK, name(), 1); + return SetupStatus.ERRORED; + } + } + + /** + * @return Retrieves the setup status + * @throws SetupException if an error occurred + */ + protected abstract SetupStatus getStatus() throws SetupException; + + /** + * Sends a start event + * + * @param action the action + * @param target the target + * @param progress the progress + */ + protected void sendStartEvent(final SetupAction action, final String target, final double progress) { + sendStartEvent(new SetupEventImpl(action, target, progress, this)); + } + + /** + * Sends an end event + * + * @param action the action + * @param target the target + * @param progress the progress + */ + protected void sendEndEvent(final SetupAction action, final String target, final double progress) { + sendEndEvent(new SetupEventImpl(action, target, progress, this)); + } + + /** + * Sends a start event + * + * @param event the event + */ + protected void sendStartEvent(final SetupEvent event) { + listeners.forEach(listener -> listener.onActionStart(event)); + } + + /** + * Sends an end event + * + * @param event the event + */ + protected void sendEndEvent(final SetupEvent event) { + listeners.forEach(listener -> listener.onActionEnd(event)); + } + + /** + * Deletes a folder + * + * @param path the path + * @throws SetupException if an error occurred + */ + protected void deleteFolder(final Path path) throws SetupException { + logger.info("Deleting {}", path); + final var index = new AtomicInteger(0); + try (final var files = Files.walk(path)) { + final var total = getFilesCount(path); + files.sorted(Comparator.reverseOrder()) + .forEach(f -> { + try { + final var progress = index.get() / (double) total; + sendStartEvent(SetupAction.DELETE, f.toString(), progress); + Files.deleteIfExists(f); + final var newProgress = index.incrementAndGet() / (double) total; + sendEndEvent(SetupAction.DELETE, f.toString(), newProgress); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (final IOException | UncheckedIOException e) { + throw new SetupException(e); + } + logger.info("{} deleted", path); + } + + private static long getFilesCount(final Path path) throws IOException { + try (final var stream = Files.walk(path)) { + return stream.count(); + } + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/impl/SetupEventImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/setup/impl/SetupEventImpl.java new file mode 100644 index 0000000..ba4db73 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/setup/impl/SetupEventImpl.java @@ -0,0 +1,20 @@ +package com.github.gtache.autosubtitle.setup.impl; + +import com.github.gtache.autosubtitle.setup.SetupAction; +import com.github.gtache.autosubtitle.setup.SetupEvent; +import com.github.gtache.autosubtitle.setup.SetupManager; + +import java.util.Objects; + +/** + * Implementation of {@link SetupEvent} + */ +public record SetupEventImpl(SetupAction action, String target, double progress, + SetupManager setupManager) implements SetupEvent { + + public SetupEventImpl { + Objects.requireNonNull(action); + Objects.requireNonNull(target); + Objects.requireNonNull(setupManager); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/AbstractSubtitleExtractor.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/AbstractSubtitleExtractor.java new file mode 100644 index 0000000..303091a --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/AbstractSubtitleExtractor.java @@ -0,0 +1,47 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.ExtractEvent; +import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor; +import com.github.gtache.autosubtitle.subtitle.SubtitleExtractorListener; + +import java.util.HashSet; +import java.util.Set; + +/** + * Base implementation of {@link SubtitleExtractor} + */ +public abstract class AbstractSubtitleExtractor implements SubtitleExtractor { + + private final Set listeners; + + /** + * Instantiates the extractor + */ + protected AbstractSubtitleExtractor() { + this.listeners = new HashSet<>(); + } + + @Override + public void addListener(final SubtitleExtractorListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(final SubtitleExtractorListener listener) { + listeners.remove(listener); + } + + @Override + public void removeListeners() { + listeners.clear(); + } + + /** + * Notifies all listeners + * + * @param event The event + */ + protected void notifyListeners(final ExtractEvent event) { + listeners.forEach(listener -> listener.listen(event)); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/ExtractEventImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/ExtractEventImpl.java new file mode 100644 index 0000000..acc7b4c --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/ExtractEventImpl.java @@ -0,0 +1,10 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.ExtractEvent; + +/** + * Implementation of {@link ExtractEvent} + */ +public record ExtractEventImpl(String message, double progress) implements ExtractEvent { + +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java index d370ebd..7c52e2a 100644 --- a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java @@ -1,10 +1,14 @@ package com.github.gtache.autosubtitle.subtitle.impl; +import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.SubtitleConverter; import javax.inject.Inject; import javax.inject.Singleton; +import java.util.Comparator; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Converts subtitles to SRT format @@ -17,7 +21,13 @@ public class SRTSubtitleConverter implements SubtitleConverter { } public String convert(final SubtitleCollection collection) { - throw new UnsupportedOperationException("TODO"); + 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); + return (i + 1) + "\n" + + subtitle.start() + " --> " + subtitle.end() + "\n" + + subtitle.content(); + }).collect(Collectors.joining("\n\n")); } @Override 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 f021551..194b5f6 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 @@ -1,22 +1,24 @@ package com.github.gtache.autosubtitle.subtitle.impl; +import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import java.util.Collection; import java.util.List; -import java.util.Locale; +import java.util.Objects; import static java.util.Objects.requireNonNull; /** * Implementation of {@link SubtitleCollection} */ -public record SubtitleCollectionImpl(Collection subtitles, - Locale locale) implements SubtitleCollection { +public record SubtitleCollectionImpl(String text, Collection subtitles, + Language language) implements SubtitleCollection { public SubtitleCollectionImpl { + Objects.requireNonNull(text); subtitles = List.copyOf(subtitles); - requireNonNull(locale); + requireNonNull(language); } } 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 new file mode 100644 index 0000000..acb864b --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleImpl.java @@ -0,0 +1,25 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.Bounds; +import com.github.gtache.autosubtitle.subtitle.Font; +import com.github.gtache.autosubtitle.subtitle.Subtitle; + +import java.util.Objects; + +/** + * Implementation of {@link Subtitle} + */ +public record SubtitleImpl(String content, long start, long end, Font font, Bounds bounds) implements Subtitle { + public SubtitleImpl { + Objects.requireNonNull(content); + if (start < 0) { + throw new IllegalArgumentException("start must be >= 0 : " + start); + } + if (end < 0) { + throw new IllegalArgumentException("end must be >= 0 : " + end); + } + if (start > end) { + throw new IllegalArgumentException("start must be <= end : " + start + " > " + end); + } + } +} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index d42c585..39e335b 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -8,9 +8,11 @@ module com.github.gtache.autosubtitle.core { requires org.apache.logging.log4j; exports com.github.gtache.autosubtitle.impl; - exports com.github.gtache.autosubtitle.modules.impl; exports com.github.gtache.autosubtitle.process.impl; + exports com.github.gtache.autosubtitle.setup.impl; exports com.github.gtache.autosubtitle.subtitle.impl; - exports com.github.gtache.autosubtitle.setup.modules.impl; - exports com.github.gtache.autosubtitle.subtitle.modules.impl; + + exports com.github.gtache.autosubtitle.modules.impl; + exports com.github.gtache.autosubtitle.modules.setup.impl; + exports com.github.gtache.autosubtitle.modules.subtitles.impl; } \ 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 17cdaaa..3fa8cb4 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 @@ -1,11 +1,11 @@ package com.github.gtache.autosubtitle.deepl; +import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.Translator; import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import javax.inject.Inject; -import java.util.Locale; /** * DeepL implementation of {@link Translator} @@ -17,22 +17,22 @@ public class DeepLTranslator implements Translator { } @Override - public Locale getLocale(final String text) { + public Language getLanguage(final String text) { return null; } @Override - public String translate(final String text, final Locale to) { + public String translate(final String text, final Language to) { return ""; } @Override - public Subtitle translate(final Subtitle subtitle, final Locale to) { + public Subtitle translate(final Subtitle subtitle, final Language to) { return null; } @Override - public SubtitleCollection translate(final SubtitleCollection collection, final Locale to) { + public SubtitleCollection translate(final SubtitleCollection collection, final Language to) { return null; } } diff --git a/deepl/src/main/java/com/github/gtache/autosubtitle/modules/deepl/DeepLModule.java b/deepl/src/main/java/com/github/gtache/autosubtitle/modules/deepl/DeepLModule.java index 801ed67..d649a41 100644 --- a/deepl/src/main/java/com/github/gtache/autosubtitle/modules/deepl/DeepLModule.java +++ b/deepl/src/main/java/com/github/gtache/autosubtitle/modules/deepl/DeepLModule.java @@ -5,8 +5,6 @@ import com.github.gtache.autosubtitle.deepl.DeepLTranslator; import dagger.Binds; import dagger.Module; -import javax.inject.Singleton; - /** * Dagger module for DeepL */ @@ -14,6 +12,5 @@ import javax.inject.Singleton; public interface DeepLModule { @Binds - @Singleton Translator bindsTranslator(final DeepLTranslator translator); } 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 abd2ec9..0966e6a 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 @@ -71,7 +71,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video args.add("-map"); args.add(String.valueOf(n)); args.add("-metadata:s:s:" + n); - args.add("language=" + c.locale().getISO3Language()); + args.add("language=" + c.language().iso3()); }); args.add(out.toString()); run(args); @@ -114,7 +114,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video ); run(args); Files.deleteIfExists(dumpVideoPath); - return new FileAudioImpl(audioPath, new AudioInfoImpl("wav")); + return new FileAudioImpl(audioPath, new AudioInfoImpl("wav", video.info().duration())); } private static Path getPath(final Video video) throws IOException { diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegModule.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegModule.java index b005dbb..db82566 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegModule.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/ffmpeg/FFmpegModule.java @@ -7,17 +7,12 @@ import com.github.gtache.autosubtitle.ffmpeg.FFprobeVideoLoader; import dagger.Binds; import dagger.Module; -import javax.inject.Singleton; - @Module public interface FFmpegModule { @Binds - @Singleton VideoConverter bindsVideoConverter(final FFmpegVideoConverter converter); @Binds - @Singleton VideoLoader bindsVideoLoader(final FFprobeVideoLoader loader); - } diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/modules/ffmpeg/FFmpegSetupModule.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java similarity index 94% rename from ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/modules/ffmpeg/FFmpegSetupModule.java rename to ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java index 3ef7b2c..c80d7cb 100644 --- a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/modules/ffmpeg/FFmpegSetupModule.java +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/modules/setup/ffmpeg/FFmpegSetupModule.java @@ -1,4 +1,4 @@ -package com.github.gtache.autosubtitle.setup.modules.ffmpeg; +package com.github.gtache.autosubtitle.modules.setup.ffmpeg; import com.github.gtache.autosubtitle.modules.ffmpeg.FFBundledRoot; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath; @@ -7,9 +7,9 @@ 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.VideoConverterSetup; import com.github.gtache.autosubtitle.setup.SetupManager; import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager; -import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverterSetup; import dagger.Binds; import dagger.Module; import dagger.Provides; 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 eec7f82..4df0850 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 @@ -5,10 +5,9 @@ import com.github.gtache.autosubtitle.impl.OS; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion; -import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner; import com.github.gtache.autosubtitle.setup.SetupException; -import com.github.gtache.autosubtitle.setup.SetupManager; import com.github.gtache.autosubtitle.setup.SetupStatus; +import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,7 +20,7 @@ import java.util.Objects; /** * Manager managing the FFmpeg installation */ -public class FFmpegSetupManager extends AbstractProcessRunner implements SetupManager { +public class FFmpegSetupManager extends AbstractSetupManager { private static final Logger logger = LogManager.getLogger(FFmpegSetupManager.class); private final Path bundledPath; private final Path systemPath; @@ -46,16 +45,17 @@ public class FFmpegSetupManager extends AbstractProcessRunner implements SetupMa } @Override - public SetupStatus status() { + public SetupStatus getStatus() throws SetupException { try { - if (checkSystemFFmpeg() || checkBundledFFmpeg()) { - return SetupStatus.INSTALLED; + if (checkSystemFFmpeg()) { + return SetupStatus.SYSTEM_INSTALLED; + } else if (checkBundledFFmpeg()) { + return SetupStatus.BUNDLE_INSTALLED; } else { return SetupStatus.NOT_INSTALLED; } } catch (final IOException e) { - logger.error("Error checking status of {}", name(), e); - return SetupStatus.ERRORED; + throw new SetupException(e); } } @@ -75,7 +75,7 @@ public class FFmpegSetupManager extends AbstractProcessRunner implements SetupMa } private boolean checkSystemFFmpeg() throws IOException { - final var result = run(systemPath.toString(), "-h"); + final var result = run(systemPath.toString(), "-version"); return result.exitCode() == 0; } diff --git a/ffmpeg/src/main/java/module-info.java b/ffmpeg/src/main/java/module-info.java index 0faf6e0..0295b4d 100644 --- a/ffmpeg/src/main/java/module-info.java +++ b/ffmpeg/src/main/java/module-info.java @@ -8,7 +8,8 @@ module com.github.gtache.autosubtitle.ffmpeg { requires org.apache.logging.log4j; exports com.github.gtache.autosubtitle.ffmpeg; - exports com.github.gtache.autosubtitle.modules.ffmpeg; exports com.github.gtache.autosubtitle.setup.ffmpeg; - exports com.github.gtache.autosubtitle.setup.modules.ffmpeg; + + exports com.github.gtache.autosubtitle.modules.ffmpeg; + exports com.github.gtache.autosubtitle.modules.setup.ffmpeg; } \ No newline at end of file diff --git a/fx/pom.xml b/fx/pom.xml index 48b3664..f916ab0 100644 --- a/fx/pom.xml +++ b/fx/pom.xml @@ -42,6 +42,11 @@ javafx-fxml ${javafx.version} + + org.controlsfx + controlsfx + 11.2.1 + \ No newline at end of file diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/AbstractFXController.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/AbstractFXController.java new file mode 100644 index 0000000..4b74315 --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/AbstractFXController.java @@ -0,0 +1,30 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.stage.Window; + +/** + * Base class for FX controllers + */ +public abstract class AbstractFXController { + + /** + * @return the current window + */ + protected abstract Window window(); + + /** + * Show an error dialog + * + * @param title the dialog title + * @param message the error message + */ + protected void showErrorDialog(final String title, final String message) { + final var alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK); + alert.initOwner(window()); + alert.setHeaderText(null); + alert.setTitle(title); + alert.showAndWait(); + } +} diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java new file mode 100644 index 0000000..1fb24a0 --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/ColonTimeFormatter.java @@ -0,0 +1,65 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.gui.TimeFormatter; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * {@link TimeFormatter} separating values using a colon + */ +@Singleton +public class ColonTimeFormatter implements TimeFormatter { + + @Inject + ColonTimeFormatter() { + + } + + @Override + public String format(final long elapsed, final long total) { + final var elapsedString = format(elapsed); + final var totalString = format(total); + return elapsedString + "/" + totalString; + } + + @Override + 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; + if (durationHours > 0) { + intDuration -= durationHours * secondsInHour; + } + final var durationMinutes = intDuration / secondsInMinute; + final var durationSeconds = intDuration - durationHours * secondsInHour + - durationMinutes * secondsInMinute; + if (durationHours > 0) { + return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds); + } else { + return String.format("%02d:%02d", durationMinutes, durationSeconds); + } + } + + @Override + public long parse(final String time) { + final var split = time.split(":"); + 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; + default -> 0; + }; + } + + private long toLong(final String time) { + if (time.startsWith("0")) { + return Long.parseLong(time.substring(1)); + } else { + return Long.parseLong(time); + } + } +} diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java index 8b68f54..3176cea 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaBinder.java @@ -23,6 +23,6 @@ public class FXMediaBinder { public void createBindings() { mediaModel.videoProperty().bindBidirectional(workModel.videoProperty()); - Bindings.bindContent(workModel.subtitles(), mediaModel.subtitles()); + Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles()); } } diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java index 4652165..9ca0639 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaController.java @@ -1,6 +1,7 @@ package com.github.gtache.autosubtitle.gui.fx; import com.github.gtache.autosubtitle.gui.MediaController; +import com.github.gtache.autosubtitle.gui.TimeFormatter; import com.github.gtache.autosubtitle.impl.FileVideoImpl; import com.github.gtache.autosubtitle.modules.gui.Pause; import com.github.gtache.autosubtitle.modules.gui.Play; @@ -51,14 +52,19 @@ public class FXMediaController implements MediaController { private Label volumeValueLabel; private final FXMediaModel model; + private final FXMediaBinder binder; + private final TimeFormatter timeFormatter; private final Image playImage; private final Image pauseImage; private boolean wasPlaying; @Inject - FXMediaController(final FXMediaModel model, @Play final Image playImage, @Pause final Image pauseImage) { + FXMediaController(final FXMediaModel model, final FXMediaBinder binder, final TimeFormatter timeFormatter, + @Play final Image playImage, @Pause final Image pauseImage) { this.model = requireNonNull(model); + this.binder = requireNonNull(binder); + this.timeFormatter = requireNonNull(timeFormatter); this.playImage = requireNonNull(playImage); this.pauseImage = requireNonNull(pauseImage); } @@ -66,7 +72,7 @@ public class FXMediaController implements MediaController { @FXML private void initialize() { volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) (model.volume() * 100)), model.volumeProperty())); - playLabel.textProperty().bind(Bindings.createStringBinding(() -> formatTime(model.position(), model.duration()), model.positionProperty(), model.durationProperty())); + playLabel.textProperty().bind(Bindings.createStringBinding(() -> timeFormatter.format(model.position(), model.duration()), model.positionProperty(), model.durationProperty())); model.positionProperty().bindBidirectional(playSlider.valueProperty()); model.volumeProperty().addListener((observable, oldValue, newValue) -> volumeSlider.setValue(newValue.doubleValue() * 100)); @@ -97,6 +103,7 @@ public class FXMediaController implements MediaController { final var millis = newTime.toMillis(); playSlider.setValue(millis); model.subtitles().forEach(s -> { + //TODO optimize if (s.start() <= millis && s.end() >= millis) { final var label = createDraggableLabel(s); stackPane.getChildren().add(label); @@ -144,6 +151,7 @@ public class FXMediaController implements MediaController { view.setFitHeight(24); return view; }, model.isPlayingProperty())); + binder.createBindings(); } @FXML @@ -190,26 +198,4 @@ public class FXMediaController implements MediaController { label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND)); return label; } - - private static String formatTime(final long position, final long duration) { - final var positionString = formatTime(position); - final var durationString = formatTime(duration); - return positionString + "/" + durationString; - } - - private static String formatTime(final long time) { - var intDuration = (int) time / 1000; - final var durationHours = intDuration / (60 * 60); - if (durationHours > 0) { - intDuration -= durationHours * 60 * 60; - } - final var durationMinutes = intDuration / 60; - final var durationSeconds = intDuration - durationHours * 60 * 60 - - durationMinutes * 60; - if (durationHours > 0) { - return String.format("%d:%02d:%02d", durationHours, durationMinutes, durationSeconds); - } else { - return String.format("%02d:%02d", durationMinutes, durationSeconds); - } - } } diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java index 385cbba..74c7498 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMediaModel.java @@ -6,10 +6,10 @@ import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javax.inject.Inject; import javax.inject.Singleton; +import java.util.List; /** * FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel} @@ -22,7 +22,7 @@ public class FXMediaModel implements MediaModel { private final BooleanProperty isPlaying; private final ReadOnlyLongWrapper duration; private final LongProperty position; - private final ObservableList subtitles; + private final List subtitles; @Inject FXMediaModel() { @@ -103,7 +103,7 @@ public class FXMediaModel implements MediaModel { } @Override - public ObservableList subtitles() { + public List subtitles() { return subtitles; } diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java new file mode 100644 index 0000000..f61e77b --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersController.java @@ -0,0 +1,130 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.gui.ParametersController; +import com.github.gtache.autosubtitle.subtitle.ExtractionModel; +import com.github.gtache.autosubtitle.subtitle.ExtractionModelProvider; +import com.github.gtache.autosubtitle.subtitle.OutputFormat; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; +import javafx.stage.Window; +import javafx.util.converter.IntegerStringConverter; +import javafx.util.converter.NumberStringConverter; +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.util.function.UnaryOperator; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +import static java.util.Objects.requireNonNull; + +/** + * FX implementation of {@link ParametersController} + */ +@Singleton +public class FXParametersController extends AbstractFXController implements ParametersController { + + private static final Logger logger = LogManager.getLogger(FXParametersController.class); + @FXML + private PrefixSelectionComboBox extractionModelCombobox; + @FXML + private PrefixSelectionComboBox extractionOutputFormat; + @FXML + private PrefixSelectionComboBox fontFamilyCombobox; + @FXML + private TextField fontSizeField; + + private final FXParametersModel model; + private final Preferences preferences; + private final ExtractionModelProvider extractionModelProvider; + + @Inject + FXParametersController(final FXParametersModel model, final Preferences preferences, final ExtractionModelProvider extractionModelProvider) { + this.model = requireNonNull(model); + this.preferences = requireNonNull(preferences); + this.extractionModelProvider = requireNonNull(extractionModelProvider); + } + + @FXML + private void initialize() { + extractionModelCombobox.setItems(model.availableExtractionModels()); + extractionModelCombobox.valueProperty().bindBidirectional(model.extractionModelProperty()); + + extractionOutputFormat.setItems(model.availableOutputFormats()); + extractionOutputFormat.valueProperty().bindBidirectional(model.outputFormatProperty()); + + fontFamilyCombobox.setItems(model.availableFontFamilies()); + fontFamilyCombobox.valueProperty().bindBidirectional(model.fontFamilyProperty()); + + final UnaryOperator integerFilter = change -> { + final var newText = change.getControlNewText(); + if (newText.matches("[1-9]\\d*")) { + return change; + } + return null; + }; + fontSizeField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, integerFilter)); + + fontSizeField.textProperty().bindBidirectional(model.fontSizeProperty(), new NumberStringConverter()); + + loadPreferences(); + } + + private void loadPreferences() { + final var extractionModel = preferences.get("extractionModel", model.extractionModel().name()); + final var outputFormat = preferences.get("outputFormat", model.outputFormat().name()); + final var fontFamily = preferences.get("fontFamily", model.fontFamily()); + final var fontSize = preferences.getInt("fontSize", model.fontSize()); + + model.setExtractionModel(extractionModelProvider.getExtractionModel(extractionModel)); + model.setOutputFormat(OutputFormat.valueOf(outputFormat)); + model.setFontFamily(fontFamily); + model.setFontSize(fontSize); + logger.info("Loaded preferences"); + } + + + @Override + public void save() { + logger.info("Saving preferences"); + preferences.put("extractionModel", model.extractionModel().name()); + preferences.put("outputFormat", model.outputFormat().name()); + preferences.put("fontFamily", model.fontFamily()); + preferences.putInt("fontSize", model.fontSize()); + try { + preferences.flush(); + logger.info("Preferences saved"); + } catch (final BackingStoreException e) { + logger.error("Error saving preferences", e); + } + } + + @Override + public void reset() { + loadPreferences(); + } + + @Override + public FXParametersModel model() { + return model; + } + + @FXML + private void savePressed() { + save(); + } + + @FXML + private void resetPressed() { + reset(); + } + + @Override + protected Window window() { + return extractionModelCombobox.getScene().getWindow(); + } +} diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersModel.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersModel.java new file mode 100644 index 0000000..dbaa438 --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXParametersModel.java @@ -0,0 +1,116 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.gui.ParametersModel; +import com.github.gtache.autosubtitle.modules.gui.FontFamily; +import com.github.gtache.autosubtitle.modules.gui.FontSize; +import com.github.gtache.autosubtitle.subtitle.ExtractionModel; +import com.github.gtache.autosubtitle.subtitle.ExtractionModelProvider; +import com.github.gtache.autosubtitle.subtitle.OutputFormat; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * FX implementation of {@link ParametersModel} + */ +@Singleton +public class FXParametersModel implements ParametersModel { + + private final ObservableList availableExtractionModels; + private final ObjectProperty extractionModel; + private final ObservableList availableOutputFormats; + private final ObjectProperty outputFormat; + private final ObservableList availableFontFamilies; + private final StringProperty fontFamily; + private final IntegerProperty fontSize; + + @Inject + FXParametersModel(final ExtractionModelProvider extractionModelProvider, @FontFamily final String defaultFontFamily, @FontSize final int defaultFontSize) { + this.availableExtractionModels = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(extractionModelProvider.getAvailableExtractionModels())); + this.extractionModel = new SimpleObjectProperty<>(extractionModelProvider.getDefaultExtractionModel()); + this.availableOutputFormats = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(OutputFormat.SRT)); + this.outputFormat = new SimpleObjectProperty<>(OutputFormat.SRT); + this.availableFontFamilies = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList("Arial")); + this.fontFamily = new SimpleStringProperty(defaultFontFamily); + this.fontSize = new SimpleIntegerProperty(defaultFontSize); + } + + @Override + public ObservableList availableExtractionModels() { + return availableExtractionModels; + } + + @Override + public ExtractionModel extractionModel() { + return extractionModel.get(); + } + + @Override + public void setExtractionModel(final ExtractionModel model) { + extractionModel.set(model); + } + + ObjectProperty extractionModelProperty() { + return extractionModel; + } + + @Override + public ObservableList availableOutputFormats() { + return availableOutputFormats; + } + + @Override + public OutputFormat outputFormat() { + return outputFormat.get(); + } + + @Override + public void setOutputFormat(final OutputFormat format) { + outputFormat.set(format); + } + + ObjectProperty outputFormatProperty() { + return outputFormat; + } + + @Override + public ObservableList availableFontFamilies() { + return availableFontFamilies; + } + + @Override + public String fontFamily() { + return fontFamily.get(); + } + + @Override + public void setFontFamily(final String fontFamily) { + this.fontFamily.set(fontFamily); + } + + StringProperty fontFamilyProperty() { + return fontFamily; + } + + @Override + public int fontSize() { + return fontSize.get(); + } + + @Override + public void setFontSize(final int fontSize) { + this.fontSize.set(fontSize); + } + + IntegerProperty fontSizeProperty() { + return fontSize; + } +} diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java index dee6dd9..1be5322 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupController.java @@ -1,17 +1,20 @@ package com.github.gtache.autosubtitle.gui.fx; import com.github.gtache.autosubtitle.gui.SetupController; +import com.github.gtache.autosubtitle.modules.setup.impl.SubtitleExtractorSetup; +import com.github.gtache.autosubtitle.modules.setup.impl.TranslatorSetup; +import com.github.gtache.autosubtitle.modules.setup.impl.VideoConverterSetup; +import com.github.gtache.autosubtitle.setup.SetupEvent; import com.github.gtache.autosubtitle.setup.SetupException; +import com.github.gtache.autosubtitle.setup.SetupListener; import com.github.gtache.autosubtitle.setup.SetupManager; import com.github.gtache.autosubtitle.setup.SetupStatus; -import com.github.gtache.autosubtitle.setup.modules.impl.SubtitleExtractorSetup; -import com.github.gtache.autosubtitle.setup.modules.impl.TranslatorSetup; -import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverterSetup; +import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.StringProperty; import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; import javafx.scene.control.Label; import javafx.scene.control.MenuButton; import javafx.scene.control.MenuItem; @@ -27,12 +30,13 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; /** * FX implementation of {@link SetupController} */ @Singleton -public class FXSetupController implements SetupController { +public class FXSetupController extends AbstractFXController implements SetupController, SetupListener { private static final Logger logger = LogManager.getLogger(FXSetupController.class); @@ -75,6 +79,8 @@ public class FXSetupController implements SetupController { private final SetupManager translatorManager; private final Map> statusMap; + private final Map setupProgressMessageMap; + private final Map setupProgressMap; @Inject FXSetupController(final FXSetupModel model, @@ -86,6 +92,8 @@ public class FXSetupController implements SetupController { this.extractorManager = Objects.requireNonNull(extractorManager); this.translatorManager = Objects.requireNonNull(translatorManager); statusMap = HashMap.newHashMap(3); + setupProgressMessageMap = HashMap.newHashMap(3); + setupProgressMap = HashMap.newHashMap(3); } @FXML @@ -93,6 +101,16 @@ public class FXSetupController implements SetupController { statusMap.put(converterManager, model.videoConverterStatusProperty()); statusMap.put(extractorManager, model.subtitleExtractorStatusProperty()); statusMap.put(translatorManager, model.translatorStatusProperty()); + setupProgressMessageMap.put(converterManager, model.videoConverterSetupProgressLabelProperty()); + setupProgressMessageMap.put(extractorManager, model.subtitleExtractorSetupProgressLabelProperty()); + setupProgressMessageMap.put(translatorManager, model.translatorSetupProgressLabelProperty()); + setupProgressMap.put(converterManager, model.videoConverterSetupProgressProperty()); + setupProgressMap.put(extractorManager, model.subtitleExtractorSetupProgressProperty()); + setupProgressMap.put(translatorManager, model.translatorSetupProgressProperty()); + + bindMenu(converterButton, converterManager); + bindMenu(extractorButton, extractorManager); + bindMenu(translatorButton, translatorManager); model.setSubtitleExtractorStatus(extractorManager.status()); model.setVideoConverterStatus(converterManager.status()); @@ -109,21 +127,17 @@ public class FXSetupController implements SetupController { extractorProgress.progressProperty().bindBidirectional(model.subtitleExtractorSetupProgressProperty()); translatorProgress.progressProperty().bindBidirectional(model.translatorSetupProgressProperty()); - converterProgress.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(0)); - extractorProgress.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(0)); - translatorProgress.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(0)); + converterProgress.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(-2)); + extractorProgress.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(-2)); + translatorProgress.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(-2)); converterProgressLabel.textProperty().bind(model.videoConverterSetupProgressLabelProperty()); extractorProgressLabel.textProperty().bind(model.subtitleExtractorSetupProgressLabelProperty()); translatorProgressLabel.textProperty().bind(model.translatorSetupProgressLabelProperty()); - converterProgressLabel.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(0)); - extractorProgressLabel.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(0)); - translatorProgressLabel.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(0)); - - bindMenu(converterButton, converterManager); - bindMenu(extractorButton, extractorManager); - bindMenu(translatorButton, translatorManager); + converterProgressLabel.visibleProperty().bind(converterProgress.visibleProperty()); + extractorProgressLabel.visibleProperty().bind(extractorProgress.visibleProperty()); + translatorProgressLabel.visibleProperty().bind(translatorProgress.visibleProperty()); } private void bindMenu(final MenuButton button, final SetupManager setupManager) { @@ -132,23 +146,23 @@ public class FXSetupController implements SetupController { button.getItems().clear(); switch (newValue) { case NOT_INSTALLED -> { - final var installItem = new MenuItem(resources.getString("setup.menu.install")); + final var installItem = new MenuItem(resources.getString("setup.menu.install.label")); installItem.setOnAction(e -> tryInstall(setupManager)); button.getItems().add(installItem); } - case INSTALLED -> { - final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall")); + case BUNDLE_INSTALLED -> { + final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall.label")); reinstallItem.setOnAction(e -> tryReinstall(setupManager)); - final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall")); + final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall.label")); uninstallItem.setOnAction(e -> tryUninstall(setupManager)); button.getItems().addAll(reinstallItem, uninstallItem); } case UPDATE_AVAILABLE -> { - final var updateItem = new MenuItem(resources.getString("setup.menu.update")); + final var updateItem = new MenuItem(resources.getString("setup.menu.update.label")); updateItem.setOnAction(e -> tryUpdate(setupManager)); - final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall")); + final var reinstallItem = new MenuItem(resources.getString("setup.menu.reinstall.label")); reinstallItem.setOnAction(e -> tryReinstall(setupManager)); - final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall")); + final var uninstallItem = new MenuItem(resources.getString("setup.menu.uninstall.label")); uninstallItem.setOnAction(e -> tryUninstall(setupManager)); button.getItems().addAll(updateItem, reinstallItem, uninstallItem); } @@ -240,33 +254,67 @@ public class FXSetupController implements SetupController { } private void trySetup(final SetupManager manager, final SetupConsumer consumer, final String operation) { - try { - consumer.accept(manager); - statusMap.get(manager).set(manager.status()); - } catch (final SetupException e) { - logger.error("Error {}ing {}", operation, manager.name(), e); - showErrorDialog(resources.getString("setup." + operation + ".error.title"), MessageFormat.format(resources.getString("setup." + operation + ".error.message"), e.getMessage())); - } + manager.addListener(this); + CompletableFuture.runAsync(() -> { + try { + consumer.accept(manager); + Platform.runLater(() -> { + statusMap.get(manager).set(manager.status()); + setupProgressMap.get(manager).set(-2); + }); + } catch (final SetupException e) { + logger.error("Error {}ing {}", operation, manager.name(), e); + Platform.runLater(() -> { + statusMap.get(manager).set(SetupStatus.ERRORED); + setupProgressMap.get(manager).set(-2); + showErrorDialog(resources.getString("setup." + operation + ".error.title"), + MessageFormat.format(resources.getString("setup." + operation + ".error.label"), e.getMessage())); + }); + } finally { + manager.removeListener(this); + } + }); + } + + @Override + public void onActionStart(final SetupEvent event) { + final var action = event.action(); + final var target = event.target(); + final var display = MessageFormat.format(resources.getString("setup.event." + action.name().toLowerCase() + ".start.label"), target); + onAction(event, display); + } + + @Override + public void onActionEnd(final SetupEvent event) { + final var action = event.action(); + final var target = event.target(); + final var display = MessageFormat.format(resources.getString("setup.event." + action.name().toLowerCase() + ".end.label"), target); + onAction(event, display); + } + + private void onAction(final SetupEvent event, final String display) { + final var manager = event.setupManager(); + final var property = setupProgressMessageMap.get(manager); + final var progress = event.progress(); + final var progressProperty = setupProgressMap.get(manager); + Platform.runLater(() -> { + property.set(display); + progressProperty.set(progress); + }); } @FunctionalInterface private interface SetupConsumer { - void accept(SetupManager manager) throws SetupException; } - private static void showErrorDialog(final String title, final String message) { - final var alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK); - alert.setTitle(title); - alert.showAndWait(); - } - @Override public FXSetupModel model() { return model; } - public Window window() { + @Override + protected Window window() { return converterNameLabel.getScene().getWindow(); } } diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java index cb55fdd..200b49c 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXSetupModel.java @@ -39,20 +39,20 @@ public class FXSetupModel implements SetupModel { @Inject FXSetupModel() { - this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED); + this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED); this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false); this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false); - this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(0); + this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(-2); this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty(""); - this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED); + this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED); this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false); this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false); - this.videoConverterSetupProgress = new SimpleDoubleProperty(0); + this.videoConverterSetupProgress = new SimpleDoubleProperty(-2); this.videoConverterSetupProgressLabel = new SimpleStringProperty(""); - this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED); + this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED); this.translatorInstalled = new ReadOnlyBooleanWrapper(false); this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false); - this.translatorSetupProgress = new SimpleDoubleProperty(0); + this.translatorSetupProgress = new SimpleDoubleProperty(-2); this.translatorSetupProgressLabel = new SimpleStringProperty(""); subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus)); diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java new file mode 100644 index 0000000..30665f9 --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkBinder.java @@ -0,0 +1,22 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Objects; + +@Singleton +public class FXWorkBinder { + + private final FXWorkModel workModel; + private final FXParametersModel parametersModel; + + @Inject + FXWorkBinder(final FXWorkModel workModel, final FXParametersModel parametersModel) { + this.workModel = Objects.requireNonNull(workModel); + this.parametersModel = Objects.requireNonNull(parametersModel); + } + + void createBindings() { + workModel.extractionModelProperty().bind(parametersModel.extractionModelProperty()); + } +} diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java index 1c7c622..1e0621d 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkController.java @@ -1,38 +1,48 @@ 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.Subtitle; +import com.github.gtache.autosubtitle.subtitle.ExtractEvent; +import com.github.gtache.autosubtitle.subtitle.ExtractException; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor; +import com.github.gtache.autosubtitle.subtitle.SubtitleExtractorListener; import com.github.gtache.autosubtitle.subtitle.fx.ObservableSubtitleImpl; -import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl; +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.Alert; import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; +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.Path; import java.text.MessageFormat; -import java.util.Comparator; import java.util.List; -import java.util.Locale; import java.util.ResourceBundle; -import java.util.stream.Collectors; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.stream.Stream; import static java.util.Objects.requireNonNull; @@ -41,14 +51,14 @@ import static java.util.Objects.requireNonNull; * FX implementation of {@link WorkController} */ @Singleton -public class FXWorkController implements WorkController { +public class FXWorkController extends AbstractFXController implements WorkController, SubtitleExtractorListener { private static final Logger logger = LogManager.getLogger(FXWorkController.class); - private static final List VIDEO_EXTENSIONS = List.of(".webm", ".mkv", ".flv", ".vob", ".ogv", ".ogg", - ".drc", ".gif", ".gifv", ".mng", ".avi", ".mts", ".m2ts", ".ts", ".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", - ".viv", ".asf", ".amv", ".mp4", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".m4v", ".svi", - ".3gp", ".3g2", ".mxf", ".roq", ".nsv", ".flv", ".f4v", ".f4p", ".f4a", ".f4b"); + private static final List VIDEO_EXTENSIONS = Stream.of("webm", "mkv", "flv", "vob", "ogv", "ogg", + "drc", "gif", "gifv", "mng", "avi", "mts", "m2ts", "ts", "mov", "qt", "wmv", "yuv", "rm", "rmvb", + "viv", "asf", "amv", "mp4", "m4p", "m4v", "mpg", "mp2", "mpeg", "mpe", "mpv", "m2v", "m4v", "svi", + "3gp", "3g2", "mxf", "roq", "nsv", "flv", "f4v", "f4p", "f4a", "f4b").map(s -> "*." + s).toList(); @FXML private TextField fileField; @@ -69,56 +79,125 @@ public class FXWorkController implements WorkController { @FXML private TableColumn textColumn; @FXML - private TextField translationField; - @FXML private FXMediaController mediaController; - + @FXML + private Button addSubtitleButton; + @FXML + private PrefixSelectionComboBox languageCombobox; + @FXML + private CheckComboBox translationsCombobox; + @FXML + private Label progressLabel; + @FXML + private ProgressBar progressBar; + @FXML + private Label progressDetailLabel; @FXML private ResourceBundle resources; private final FXWorkModel model; + private final FXWorkBinder binder; private final SubtitleExtractor subtitleExtractor; private final VideoConverter videoConverter; private final VideoLoader videoLoader; private final Translator translator; - private final FXMediaBinder binder; + private final TimeFormatter timeFormatter; @Inject - FXWorkController(final FXWorkModel model, final SubtitleExtractor subtitleExtractor, final VideoLoader videoLoader, - final VideoConverter videoConverter, final Translator translator, final FXMediaBinder binder) { + FXWorkController(final FXWorkModel model, final FXWorkBinder binder, final SubtitleExtractor subtitleExtractor, final VideoLoader videoLoader, + final VideoConverter videoConverter, final Translator translator, final TimeFormatter timeFormatter) { this.model = requireNonNull(model); + this.binder = requireNonNull(binder); this.subtitleExtractor = requireNonNull(subtitleExtractor); this.videoConverter = requireNonNull(videoConverter); this.videoLoader = requireNonNull(videoLoader); this.translator = requireNonNull(translator); - this.binder = requireNonNull(binder); + this.timeFormatter = requireNonNull(timeFormatter); } @FXML private void initialize() { - extractButton.disableProperty().bind(model.videoProperty().isNull()); + 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()); + + extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE))); resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles())); - exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles())); - exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles())); + 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))); + addSubtitleButton.disableProperty().bind(model.videoProperty().isNull()); 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(); + } + }); + 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(); + }); + endColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter))); endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end())); - textColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? "" : param.getValue().content())); + endColumn.setOnEditCommit(e -> { + final var subtitle = e.getRowValue(); + subtitle.setEnd(e.getNewValue()); + subtitlesTable.refresh(); + }); + 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.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue)); model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { mediaController.seek(newValue.start()); } }); + 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)); + progressBar.progressProperty().bindBidirectional(model.progressProperty()); binder.createBindings(); + + subtitleExtractor.addListener(this); + } + + private void editFocusedCell() { + final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell(); + if (focusedCell != null) { + subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn()); + } } @FXML private void fileButtonPressed() { final var filePicker = new FileChooser(); - filePicker.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Video", VIDEO_EXTENSIONS)); + final var extensionFilter = new FileChooser.ExtensionFilter("All supported", VIDEO_EXTENSIONS); + filePicker.getExtensionFilters().add(extensionFilter); + filePicker.setSelectedExtensionFilter(extensionFilter); final var file = filePicker.showOpenDialog(window()); if (file != null) { loadVideo(file.toPath()); @@ -128,12 +207,32 @@ public class FXWorkController implements WorkController { @Override public void extractSubtitles() { if (model.video() != null) { - final var subtitles = subtitleExtractor.extract(model.video()).stream().sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList(); - final var subtitlesCopy = subtitles.stream().map(ObservableSubtitleImpl::new).toList(); - model.subtitles().setAll(subtitles); + model.setStatus(WorkStatus.EXTRACTING); + CompletableFuture.supplyAsync(this::extractAsync).whenCompleteAsync(this::manageExtractResult, Platform::runLater); + } + } + + private SubtitleCollection extractAsync() { + try { + return subtitleExtractor.extract(model.video(), model.videoLanguage(), model.extractionModel()); + } catch (final ExtractException e) { + throw new CompletionException(e); + } + } + + private void manageExtractResult(final SubtitleCollection newCollection, final Throwable t) { + if (t == null) { + final var newSubtitles = newCollection.subtitles().stream().map(ObservableSubtitleImpl::new).toList(); + final var subtitlesCopy = newSubtitles.stream().map(ObservableSubtitleImpl::new).toList(); + model.subtitles().setAll(newSubtitles); model.originalSubtitles().clear(); model.originalSubtitles().addAll(subtitlesCopy); + model.videoLanguageProperty().set(newCollection.language()); + } else { + logger.error("Error extracting subtitles", t); + showErrorDialog(resources.getString("work.extract.error.title"), MessageFormat.format(resources.getString("work.extract.error.label"), t.getMessage())); } + model.setStatus(WorkStatus.IDLE); } @Override @@ -144,6 +243,7 @@ public class FXWorkController implements WorkController { model.videoProperty().set(loadedVideo); } catch (final IOException e) { logger.error("Error loading video {}", file, e); + showErrorDialog(resources.getString("work.load.error.title"), MessageFormat.format(resources.getString("work.load.error.label"), e.getMessage())); } } @@ -152,17 +252,27 @@ public class FXWorkController implements WorkController { final var filePicker = new FileChooser(); final var file = filePicker.showSaveDialog(window()); if (file != null) { - final var text = model.subtitles().stream().map(Subtitle::content).collect(Collectors.joining(" ")); - final var baseCollection = new SubtitleCollectionImpl(model.subtitles(), translator.getLocale(text)); - final var collections = Stream.concat(Stream.of(baseCollection), model.translations().stream().map(l -> translator.translate(baseCollection, l))).toList(); - try { - videoConverter.addSoftSubtitles(model.video(), collections); - } catch (final IOException e) { - logger.error("Error exporting subtitles", e); - final var alert = new Alert(Alert.AlertType.ERROR, MessageFormat.format(resources.getString("work.error.export.label"), e.getMessage()), ButtonType.OK); - alert.setTitle(resources.getString("work.error.export.title")); - alert.showAndWait(); - } + 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); + } 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); } } @@ -171,14 +281,19 @@ public class FXWorkController implements WorkController { final var filePicker = new FileChooser(); final var file = filePicker.showSaveDialog(window()); if (file != null) { - try { - videoConverter.addHardSubtitles(model.video(), new SubtitleCollectionImpl(model.subtitles(), Locale.getDefault())); - } catch (final IOException e) { - logger.error("Error exporting subtitles", e); - final var alert = new Alert(Alert.AlertType.ERROR, MessageFormat.format(resources.getString("work.error.export.label"), e.getMessage()), ButtonType.OK); - alert.setTitle(resources.getString("work.error.export.title")); - alert.showAndWait(); - } + CompletableFuture.runAsync(() -> { + try { + videoConverter.addHardSubtitles(model.video(), model.subtitleCollection()); + } 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); } } @@ -201,4 +316,17 @@ public class FXWorkController implements WorkController { 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(() -> { + model.setProgress(event.progress()); + progressDetailLabel.setText(event.message()); + }); + } } diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java index fb3aa9e..239afc6 100644 --- a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXWorkModel.java @@ -1,9 +1,21 @@ package com.github.gtache.autosubtitle.gui.fx; +import com.github.gtache.autosubtitle.Language; import com.github.gtache.autosubtitle.Video; import com.github.gtache.autosubtitle.gui.WorkModel; +import com.github.gtache.autosubtitle.gui.WorkStatus; import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; +import com.github.gtache.autosubtitle.subtitle.ExtractionModel; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; +import com.github.gtache.autosubtitle.subtitle.impl.SubtitleCollectionImpl; +import javafx.beans.binding.Bindings; +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.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -11,8 +23,10 @@ import javafx.collections.ObservableList; 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.Locale; +import java.util.stream.Collectors; /** * FX implementation of {@link WorkModel} @@ -21,18 +35,52 @@ import java.util.Locale; public class FXWorkModel implements WorkModel { private final ObjectProperty