Extraction works

This commit is contained in:
Guillaume Tâche
2024-08-04 21:55:30 +02:00
parent 8002fc6719
commit 5efdaa6f63
121 changed files with 3360 additions and 400 deletions

4
.gitignore vendored
View File

@@ -36,4 +36,6 @@ build/
.vscode/ .vscode/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
tools

2
.idea/encodings.xml generated
View File

@@ -3,6 +3,8 @@
<component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="ISO-8859-1"> <component name="Encoding" native2AsciiForPropertiesFiles="true" defaultCharsetForPropertiesFiles="ISO-8859-1">
<file url="file://$PROJECT_DIR$/api/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/api/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/cli/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/cli/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/core/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/core/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/core/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/deepl/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/deepl/src/main/java" charset="UTF-8" />

View File

@@ -7,5 +7,10 @@ public interface AudioInfo {
/** /**
* @return The audio extension (mp3, etc.) * @return The audio extension (mp3, etc.)
*/ */
String videoFormat(); String audioFormat();
/**
* @return The audio duration in milliseconds
*/
long duration();
} }

View File

@@ -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> STRING_LANGUAGE_MAP;
static {
final Map<String, Language> 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<String> 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);
}
}

View File

@@ -3,88 +3,86 @@ package com.github.gtache.autosubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import java.util.Locale;
/** /**
* Translates texts and subtitles * Translates texts and subtitles
*/ */
public interface Translator { public interface Translator {
/** /**
* Guesses the locale of the given text * Guesses the language of the given text
* *
* @param text The 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 * @param subtitle The subtitle
* @return The guessed locale * @return The guessed language
*/ */
default Locale getLocale(final Subtitle subtitle) { default Language getLanguage(final Subtitle subtitle) {
return getLocale(subtitle.content()); 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 text The text to translate
* @param to The target locale * @param to The target language
* @return The translated text * @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 text The text to translate
* @param to The target locale * @param to The target language
* @return The translated text * @return The translated text
*/ */
default String translate(final String text, final String to) { 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 subtitle The subtitle to translate
* @param to The target locale * @param to The target language
* @return The translated subtitle * @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 subtitle The subtitle to translate
* @param to The target locale * @param to The target language
* @return The translated subtitle * @return The translated subtitle
*/ */
default Subtitle translate(final Subtitle subtitle, final String to) { 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 collection The subtitles collection to translate
* @param to The target locale * @param to The target language
* @return The translated subtitles collection * @return The translated subtitles collection
*/ */
default SubtitleCollection translate(final SubtitleCollection collection, final String to) { 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 collection The subtitles collection to translate
* @param to The target locale * @param to The target language
* @return The translated subtitles collection * @return The translated subtitles collection
*/ */
SubtitleCollection translate(SubtitleCollection collection, Locale to); SubtitleCollection translate(SubtitleCollection collection, Language to);
} }

View File

@@ -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;
}

View File

@@ -28,4 +28,44 @@ public interface ProcessRunner {
* @throws IOException if something goes wrong * @throws IOException if something goes wrong
*/ */
ProcessResult run(final List<String> args) throws IOException; ProcessResult run(final List<String> 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<String> 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<String> args) throws IOException;
} }

View File

@@ -0,0 +1,8 @@
package com.github.gtache.autosubtitle.setup;
/**
* Represents a setup action
*/
public enum SetupAction {
CHECK, DOWNLOAD, INSTALL, UNINSTALL, UPDATE, DELETE
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -17,11 +17,9 @@ public interface SetupManager {
/** /**
* @return whether the component is installed * @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 { boolean isInstalled() throws SetupException;
return status().isInstalled();
}
/** /**
* Installs the component * Installs the component
@@ -42,10 +40,7 @@ public interface SetupManager {
* *
* @throws SetupException if an error occurred during the reinstallation * @throws SetupException if an error occurred during the reinstallation
*/ */
default void reinstall() throws SetupException { void reinstall() throws SetupException;
uninstall();
install();
}
/** /**
* Checks if an update is available for the component * Checks if an update is available for the component
@@ -53,9 +48,7 @@ public interface SetupManager {
* @return whether an update is available * @return whether an update is available
* @throws SetupException if an error occurred during the check * @throws SetupException if an error occurred during the check
*/ */
default boolean isUpdateAvailable() throws SetupException { boolean isUpdateAvailable() throws SetupException;
return status() == SetupStatus.UPDATE_AVAILABLE;
}
/** /**
* Updates the component * Updates the component
@@ -63,4 +56,23 @@ public interface SetupManager {
* @throws SetupException if an error occurred during the update * @throws SetupException if an error occurred during the update
*/ */
void update() throws SetupException; 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();
} }

View File

@@ -4,9 +4,9 @@ package com.github.gtache.autosubtitle.setup;
* The status of a setup * The status of a setup
*/ */
public enum SetupStatus { public enum SetupStatus {
ERRORED, NOT_INSTALLED, INSTALLED, UPDATE_AVAILABLE; ERRORED, NOT_INSTALLED, SYSTEM_INSTALLED, BUNDLE_INSTALLED, UPDATE_AVAILABLE;
public boolean isInstalled() { public boolean isInstalled() {
return this == INSTALLED || this == UPDATE_AVAILABLE; return this == SYSTEM_INSTALLED || this == BUNDLE_INSTALLED || this == UPDATE_AVAILABLE;
} }
} }

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
package com.github.gtache.autosubtitle.subtitle;
/**
* An extraction model
*/
public interface ExtractionModel {
/**
* @return the name of the model
*/
String name();
}

View File

@@ -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<ExtractionModel> 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);
}

View File

@@ -0,0 +1,8 @@
package com.github.gtache.autosubtitle.subtitle;
/**
* The possible subtitles output formats
*/
public enum OutputFormat {
SRT, ASS
}

View File

@@ -1,7 +1,8 @@
package com.github.gtache.autosubtitle.subtitle; package com.github.gtache.autosubtitle.subtitle;
import com.github.gtache.autosubtitle.Language;
import java.util.Collection; import java.util.Collection;
import java.util.Locale;
/** /**
* Represents a collection of {@link Subtitle} * Represents a collection of {@link Subtitle}
@@ -14,7 +15,7 @@ public interface SubtitleCollection {
Collection<? extends Subtitle> subtitles(); Collection<? extends Subtitle> subtitles();
/** /**
* @return The locale of the subtitles * @return The language of the subtitles
*/ */
Locale locale(); Language language();
} }

View File

@@ -1,16 +1,76 @@
package com.github.gtache.autosubtitle.subtitle; package com.github.gtache.autosubtitle.subtitle;
import com.github.gtache.autosubtitle.Audio; import com.github.gtache.autosubtitle.Audio;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video; import com.github.gtache.autosubtitle.Video;
import java.util.Collection;
/** /**
* Extracts subtitles from a video or audio * Extracts subtitles from a video or audio
*/ */
public interface SubtitleExtractor { public interface SubtitleExtractor {
Collection<? extends EditableSubtitle> extract(final Video in); /**
* Adds a listener
*
* @param listener The listener
*/
void addListener(SubtitleExtractorListener listener);
Collection<? extends EditableSubtitle> 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;
} }

View File

@@ -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);
}

41
cli/pom.xml Normal file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>autosubtitle-cli</artifactId>
<dependencies>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-deepl</artifactId>
</dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-ffmpeg</artifactId>
</dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-whisper</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<String> 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));
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -4,7 +4,17 @@ package com.github.gtache.autosubtitle.impl;
* The list of possible operating systems * The list of possible operating systems
*/ */
public enum Architecture { 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) { public static Architecture getArchitecture(final String name) {
try { try {
@@ -13,4 +23,12 @@ public enum Architecture {
return UNKNOWN; 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;
}
} }

View File

@@ -7,14 +7,12 @@ import java.util.Objects;
/** /**
* Implementation of {@link AudioInfo} * Implementation of {@link AudioInfo}
*/ */
public record AudioInfoImpl(String audioFormat) implements AudioInfo { public record AudioInfoImpl(String audioFormat, long duration) implements AudioInfo {
public AudioInfoImpl { public AudioInfoImpl {
Objects.requireNonNull(audioFormat); Objects.requireNonNull(audioFormat);
} if (duration < 0) {
throw new IllegalArgumentException("Duration must be positive");
@Override }
public String videoFormat() {
return audioFormat;
} }
} }

View File

@@ -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 javax.inject.Qualifier;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;

View File

@@ -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 javax.inject.Qualifier;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;

View File

@@ -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 javax.inject.Qualifier;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;

View File

@@ -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.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.impl.SRTSubtitleConverter; import com.github.gtache.autosubtitle.subtitle.impl.SRTSubtitleConverter;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
import javax.inject.Singleton;
/** /**
* Dagger module for subtitle converter * Dagger module for subtitle converter
*/ */
@@ -14,6 +12,5 @@ import javax.inject.Singleton;
public interface ConverterModule { public interface ConverterModule {
@Binds @Binds
@Singleton
SubtitleConverter bindsSubtitleConverter(final SRTSubtitleConverter converter); SubtitleConverter bindsSubtitleConverter(final SRTSubtitleConverter converter);
} }

View File

@@ -1,19 +1,15 @@
package com.github.gtache.autosubtitle.process.impl; 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.ProcessResult;
import com.github.gtache.autosubtitle.process.ProcessRunner; import com.github.gtache.autosubtitle.process.ProcessRunner;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.time.Duration;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/** /**
* Base implementation of {@link ProcessRunner} * Base implementation of {@link ProcessRunner}
@@ -24,29 +20,30 @@ public abstract class AbstractProcessRunner implements ProcessRunner {
@Override @Override
public ProcessResult run(final List<String> args) throws IOException { public ProcessResult run(final List<String> args) throws IOException {
final var builder = new ProcessBuilder(args); final var listener = startListen(args);
builder.redirectErrorStream(true); CompletableFuture.runAsync(() -> {
final var process = builder.start(); try {
final var readFuture = CompletableFuture.supplyAsync(() -> { var line = listener.readLine();
final var output = new ArrayList<String>();
try (final var in = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream()), StandardCharsets.UTF_8))) {
var line = in.readLine();
while (line != null) { while (line != null) {
output.add(line); line = listener.readLine();
line = in.readLine();
} }
} catch (final IOException e) { } catch (final IOException e) {
logger.error("Error listening to process output of {}", args, e); logger.error("Error listening to process output of {}", args, e);
} }
return output;
}); });
try { return listener.join(Duration.ofHours(1));
process.waitFor(1, TimeUnit.HOURS); }
} catch (final InterruptedException e) {
Thread.currentThread().interrupt(); @Override
process.destroy(); public Process start(final List<String> args) throws IOException {
} final var builder = new ProcessBuilder(args);
final var output = readFuture.join(); builder.redirectErrorStream(true);
return new ProcessResultImpl(process.exitValue(), output); return builder.start();
}
@Override
public ProcessListener startListen(final List<String> args) throws IOException {
final var process = start(args);
return new ProcessListenerImpl(process);
} }
} }

View File

@@ -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<String> 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);
}
}

View File

@@ -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<SetupListener> 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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<SubtitleExtractorListener> 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));
}
}

View File

@@ -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 {
}

View File

@@ -1,10 +1,14 @@
package com.github.gtache.autosubtitle.subtitle.impl; 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.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleConverter; import com.github.gtache.autosubtitle.subtitle.SubtitleConverter;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.Comparator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/** /**
* Converts subtitles to SRT format * Converts subtitles to SRT format
@@ -17,7 +21,13 @@ public class SRTSubtitleConverter implements SubtitleConverter {
} }
public String convert(final SubtitleCollection collection) { 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 @Override

View File

@@ -1,22 +1,24 @@
package com.github.gtache.autosubtitle.subtitle.impl; 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.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Objects;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
/** /**
* Implementation of {@link SubtitleCollection} * Implementation of {@link SubtitleCollection}
*/ */
public record SubtitleCollectionImpl(Collection<? extends Subtitle> subtitles, public record SubtitleCollectionImpl(String text, Collection<? extends Subtitle> subtitles,
Locale locale) implements SubtitleCollection { Language language) implements SubtitleCollection {
public SubtitleCollectionImpl { public SubtitleCollectionImpl {
Objects.requireNonNull(text);
subtitles = List.copyOf(subtitles); subtitles = List.copyOf(subtitles);
requireNonNull(locale); requireNonNull(language);
} }
} }

View File

@@ -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);
}
}
}

View File

@@ -8,9 +8,11 @@ module com.github.gtache.autosubtitle.core {
requires org.apache.logging.log4j; requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.impl; 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.process.impl;
exports com.github.gtache.autosubtitle.setup.impl;
exports com.github.gtache.autosubtitle.subtitle.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;
} }

View File

@@ -1,11 +1,11 @@
package com.github.gtache.autosubtitle.deepl; package com.github.gtache.autosubtitle.deepl;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator; import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.Locale;
/** /**
* DeepL implementation of {@link Translator} * DeepL implementation of {@link Translator}
@@ -17,22 +17,22 @@ public class DeepLTranslator implements Translator {
} }
@Override @Override
public Locale getLocale(final String text) { public Language getLanguage(final String text) {
return null; return null;
} }
@Override @Override
public String translate(final String text, final Locale to) { public String translate(final String text, final Language to) {
return ""; return "";
} }
@Override @Override
public Subtitle translate(final Subtitle subtitle, final Locale to) { public Subtitle translate(final Subtitle subtitle, final Language to) {
return null; return null;
} }
@Override @Override
public SubtitleCollection translate(final SubtitleCollection collection, final Locale to) { public SubtitleCollection translate(final SubtitleCollection collection, final Language to) {
return null; return null;
} }
} }

View File

@@ -5,8 +5,6 @@ import com.github.gtache.autosubtitle.deepl.DeepLTranslator;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
import javax.inject.Singleton;
/** /**
* Dagger module for DeepL * Dagger module for DeepL
*/ */
@@ -14,6 +12,5 @@ import javax.inject.Singleton;
public interface DeepLModule { public interface DeepLModule {
@Binds @Binds
@Singleton
Translator bindsTranslator(final DeepLTranslator translator); Translator bindsTranslator(final DeepLTranslator translator);
} }

View File

@@ -71,7 +71,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
args.add("-map"); args.add("-map");
args.add(String.valueOf(n)); args.add(String.valueOf(n));
args.add("-metadata:s:s:" + n); args.add("-metadata:s:s:" + n);
args.add("language=" + c.locale().getISO3Language()); args.add("language=" + c.language().iso3());
}); });
args.add(out.toString()); args.add(out.toString());
run(args); run(args);
@@ -114,7 +114,7 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
); );
run(args); run(args);
Files.deleteIfExists(dumpVideoPath); 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 { private static Path getPath(final Video video) throws IOException {

View File

@@ -7,17 +7,12 @@ import com.github.gtache.autosubtitle.ffmpeg.FFprobeVideoLoader;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
import javax.inject.Singleton;
@Module @Module
public interface FFmpegModule { public interface FFmpegModule {
@Binds @Binds
@Singleton
VideoConverter bindsVideoConverter(final FFmpegVideoConverter converter); VideoConverter bindsVideoConverter(final FFmpegVideoConverter converter);
@Binds @Binds
@Singleton
VideoLoader bindsVideoLoader(final FFprobeVideoLoader loader); VideoLoader bindsVideoLoader(final FFprobeVideoLoader loader);
} }

View File

@@ -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.FFBundledRoot;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath; 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.FFprobeBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath; import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath;
import com.github.gtache.autosubtitle.modules.impl.ExecutableExtension; 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.SetupManager;
import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager; import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager;
import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverterSetup;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;

View File

@@ -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.FFmpegBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion; 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.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -21,7 +20,7 @@ import java.util.Objects;
/** /**
* Manager managing the FFmpeg installation * 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 static final Logger logger = LogManager.getLogger(FFmpegSetupManager.class);
private final Path bundledPath; private final Path bundledPath;
private final Path systemPath; private final Path systemPath;
@@ -46,16 +45,17 @@ public class FFmpegSetupManager extends AbstractProcessRunner implements SetupMa
} }
@Override @Override
public SetupStatus status() { public SetupStatus getStatus() throws SetupException {
try { try {
if (checkSystemFFmpeg() || checkBundledFFmpeg()) { if (checkSystemFFmpeg()) {
return SetupStatus.INSTALLED; return SetupStatus.SYSTEM_INSTALLED;
} else if (checkBundledFFmpeg()) {
return SetupStatus.BUNDLE_INSTALLED;
} else { } else {
return SetupStatus.NOT_INSTALLED; return SetupStatus.NOT_INSTALLED;
} }
} catch (final IOException e) { } catch (final IOException e) {
logger.error("Error checking status of {}", name(), e); throw new SetupException(e);
return SetupStatus.ERRORED;
} }
} }
@@ -75,7 +75,7 @@ public class FFmpegSetupManager extends AbstractProcessRunner implements SetupMa
} }
private boolean checkSystemFFmpeg() throws IOException { private boolean checkSystemFFmpeg() throws IOException {
final var result = run(systemPath.toString(), "-h"); final var result = run(systemPath.toString(), "-version");
return result.exitCode() == 0; return result.exitCode() == 0;
} }

View File

@@ -8,7 +8,8 @@ module com.github.gtache.autosubtitle.ffmpeg {
requires org.apache.logging.log4j; requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.ffmpeg; 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.ffmpeg;
exports com.github.gtache.autosubtitle.setup.modules.ffmpeg;
exports com.github.gtache.autosubtitle.modules.ffmpeg;
exports com.github.gtache.autosubtitle.modules.setup.ffmpeg;
} }

View File

@@ -42,6 +42,11 @@
<artifactId>javafx-fxml</artifactId> <artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version> <version>${javafx.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>11.2.1</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -23,6 +23,6 @@ public class FXMediaBinder {
public void createBindings() { public void createBindings() {
mediaModel.videoProperty().bindBidirectional(workModel.videoProperty()); mediaModel.videoProperty().bindBidirectional(workModel.videoProperty());
Bindings.bindContent(workModel.subtitles(), mediaModel.subtitles()); Bindings.bindContent(mediaModel.subtitles(), workModel.subtitles());
} }
} }

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle.gui.fx; package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.MediaController; 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.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.modules.gui.Pause; import com.github.gtache.autosubtitle.modules.gui.Pause;
import com.github.gtache.autosubtitle.modules.gui.Play; import com.github.gtache.autosubtitle.modules.gui.Play;
@@ -51,14 +52,19 @@ public class FXMediaController implements MediaController {
private Label volumeValueLabel; private Label volumeValueLabel;
private final FXMediaModel model; private final FXMediaModel model;
private final FXMediaBinder binder;
private final TimeFormatter timeFormatter;
private final Image playImage; private final Image playImage;
private final Image pauseImage; private final Image pauseImage;
private boolean wasPlaying; private boolean wasPlaying;
@Inject @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.model = requireNonNull(model);
this.binder = requireNonNull(binder);
this.timeFormatter = requireNonNull(timeFormatter);
this.playImage = requireNonNull(playImage); this.playImage = requireNonNull(playImage);
this.pauseImage = requireNonNull(pauseImage); this.pauseImage = requireNonNull(pauseImage);
} }
@@ -66,7 +72,7 @@ public class FXMediaController implements MediaController {
@FXML @FXML
private void initialize() { private void initialize() {
volumeValueLabel.textProperty().bind(Bindings.createStringBinding(() -> String.valueOf((int) (model.volume() * 100)), model.volumeProperty())); 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.positionProperty().bindBidirectional(playSlider.valueProperty());
model.volumeProperty().addListener((observable, oldValue, newValue) -> volumeSlider.setValue(newValue.doubleValue() * 100)); 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(); final var millis = newTime.toMillis();
playSlider.setValue(millis); playSlider.setValue(millis);
model.subtitles().forEach(s -> { model.subtitles().forEach(s -> {
//TODO optimize
if (s.start() <= millis && s.end() >= millis) { if (s.start() <= millis && s.end() >= millis) {
final var label = createDraggableLabel(s); final var label = createDraggableLabel(s);
stackPane.getChildren().add(label); stackPane.getChildren().add(label);
@@ -144,6 +151,7 @@ public class FXMediaController implements MediaController {
view.setFitHeight(24); view.setFitHeight(24);
return view; return view;
}, model.isPlayingProperty())); }, model.isPlayingProperty()));
binder.createBindings();
} }
@FXML @FXML
@@ -190,26 +198,4 @@ public class FXMediaController implements MediaController {
label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND)); label.setOnMouseEntered(mouseEvent -> label.setCursor(Cursor.HAND));
return label; 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);
}
}
} }

View File

@@ -6,10 +6,10 @@ import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.List;
/** /**
* FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel} * 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 BooleanProperty isPlaying;
private final ReadOnlyLongWrapper duration; private final ReadOnlyLongWrapper duration;
private final LongProperty position; private final LongProperty position;
private final ObservableList<EditableSubtitle> subtitles; private final List<EditableSubtitle> subtitles;
@Inject @Inject
FXMediaModel() { FXMediaModel() {
@@ -103,7 +103,7 @@ public class FXMediaModel implements MediaModel {
} }
@Override @Override
public ObservableList<EditableSubtitle> subtitles() { public List<EditableSubtitle> subtitles() {
return subtitles; return subtitles;
} }

View File

@@ -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<ExtractionModel> extractionModelCombobox;
@FXML
private PrefixSelectionComboBox<OutputFormat> extractionOutputFormat;
@FXML
private PrefixSelectionComboBox<String> 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<TextFormatter.Change> 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();
}
}

View File

@@ -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<ExtractionModel> availableExtractionModels;
private final ObjectProperty<ExtractionModel> extractionModel;
private final ObservableList<OutputFormat> availableOutputFormats;
private final ObjectProperty<OutputFormat> outputFormat;
private final ObservableList<String> 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<ExtractionModel> availableExtractionModels() {
return availableExtractionModels;
}
@Override
public ExtractionModel extractionModel() {
return extractionModel.get();
}
@Override
public void setExtractionModel(final ExtractionModel model) {
extractionModel.set(model);
}
ObjectProperty<ExtractionModel> extractionModelProperty() {
return extractionModel;
}
@Override
public ObservableList<OutputFormat> availableOutputFormats() {
return availableOutputFormats;
}
@Override
public OutputFormat outputFormat() {
return outputFormat.get();
}
@Override
public void setOutputFormat(final OutputFormat format) {
outputFormat.set(format);
}
ObjectProperty<OutputFormat> outputFormatProperty() {
return outputFormat;
}
@Override
public ObservableList<String> 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;
}
}

View File

@@ -1,17 +1,20 @@
package com.github.gtache.autosubtitle.gui.fx; package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.SetupController; 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.SetupException;
import com.github.gtache.autosubtitle.setup.SetupListener;
import com.github.gtache.autosubtitle.setup.SetupManager; import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus; import com.github.gtache.autosubtitle.setup.SetupStatus;
import com.github.gtache.autosubtitle.setup.modules.impl.SubtitleExtractorSetup; import javafx.application.Platform;
import com.github.gtache.autosubtitle.setup.modules.impl.TranslatorSetup;
import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverterSetup;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
@@ -27,12 +30,13 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
/** /**
* FX implementation of {@link SetupController} * FX implementation of {@link SetupController}
*/ */
@Singleton @Singleton
public class FXSetupController implements SetupController { public class FXSetupController extends AbstractFXController implements SetupController, SetupListener {
private static final Logger logger = LogManager.getLogger(FXSetupController.class); private static final Logger logger = LogManager.getLogger(FXSetupController.class);
@@ -75,6 +79,8 @@ public class FXSetupController implements SetupController {
private final SetupManager translatorManager; private final SetupManager translatorManager;
private final Map<SetupManager, ObjectProperty<SetupStatus>> statusMap; private final Map<SetupManager, ObjectProperty<SetupStatus>> statusMap;
private final Map<SetupManager, StringProperty> setupProgressMessageMap;
private final Map<SetupManager, DoubleProperty> setupProgressMap;
@Inject @Inject
FXSetupController(final FXSetupModel model, FXSetupController(final FXSetupModel model,
@@ -86,6 +92,8 @@ public class FXSetupController implements SetupController {
this.extractorManager = Objects.requireNonNull(extractorManager); this.extractorManager = Objects.requireNonNull(extractorManager);
this.translatorManager = Objects.requireNonNull(translatorManager); this.translatorManager = Objects.requireNonNull(translatorManager);
statusMap = HashMap.newHashMap(3); statusMap = HashMap.newHashMap(3);
setupProgressMessageMap = HashMap.newHashMap(3);
setupProgressMap = HashMap.newHashMap(3);
} }
@FXML @FXML
@@ -93,6 +101,16 @@ public class FXSetupController implements SetupController {
statusMap.put(converterManager, model.videoConverterStatusProperty()); statusMap.put(converterManager, model.videoConverterStatusProperty());
statusMap.put(extractorManager, model.subtitleExtractorStatusProperty()); statusMap.put(extractorManager, model.subtitleExtractorStatusProperty());
statusMap.put(translatorManager, model.translatorStatusProperty()); 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.setSubtitleExtractorStatus(extractorManager.status());
model.setVideoConverterStatus(converterManager.status()); model.setVideoConverterStatus(converterManager.status());
@@ -109,21 +127,17 @@ public class FXSetupController implements SetupController {
extractorProgress.progressProperty().bindBidirectional(model.subtitleExtractorSetupProgressProperty()); extractorProgress.progressProperty().bindBidirectional(model.subtitleExtractorSetupProgressProperty());
translatorProgress.progressProperty().bindBidirectional(model.translatorSetupProgressProperty()); translatorProgress.progressProperty().bindBidirectional(model.translatorSetupProgressProperty());
converterProgress.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(0)); converterProgress.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(-2));
extractorProgress.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(0)); extractorProgress.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(-2));
translatorProgress.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(0)); translatorProgress.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(-2));
converterProgressLabel.textProperty().bind(model.videoConverterSetupProgressLabelProperty()); converterProgressLabel.textProperty().bind(model.videoConverterSetupProgressLabelProperty());
extractorProgressLabel.textProperty().bind(model.subtitleExtractorSetupProgressLabelProperty()); extractorProgressLabel.textProperty().bind(model.subtitleExtractorSetupProgressLabelProperty());
translatorProgressLabel.textProperty().bind(model.translatorSetupProgressLabelProperty()); translatorProgressLabel.textProperty().bind(model.translatorSetupProgressLabelProperty());
converterProgressLabel.visibleProperty().bind(model.videoConverterSetupProgressProperty().greaterThan(0)); converterProgressLabel.visibleProperty().bind(converterProgress.visibleProperty());
extractorProgressLabel.visibleProperty().bind(model.subtitleExtractorSetupProgressProperty().greaterThan(0)); extractorProgressLabel.visibleProperty().bind(extractorProgress.visibleProperty());
translatorProgressLabel.visibleProperty().bind(model.translatorSetupProgressProperty().greaterThan(0)); translatorProgressLabel.visibleProperty().bind(translatorProgress.visibleProperty());
bindMenu(converterButton, converterManager);
bindMenu(extractorButton, extractorManager);
bindMenu(translatorButton, translatorManager);
} }
private void bindMenu(final MenuButton button, final SetupManager setupManager) { private void bindMenu(final MenuButton button, final SetupManager setupManager) {
@@ -132,23 +146,23 @@ public class FXSetupController implements SetupController {
button.getItems().clear(); button.getItems().clear();
switch (newValue) { switch (newValue) {
case NOT_INSTALLED -> { 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)); installItem.setOnAction(e -> tryInstall(setupManager));
button.getItems().add(installItem); button.getItems().add(installItem);
} }
case INSTALLED -> { case BUNDLE_INSTALLED -> {
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)); 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)); uninstallItem.setOnAction(e -> tryUninstall(setupManager));
button.getItems().addAll(reinstallItem, uninstallItem); button.getItems().addAll(reinstallItem, uninstallItem);
} }
case UPDATE_AVAILABLE -> { 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)); 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)); 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)); uninstallItem.setOnAction(e -> tryUninstall(setupManager));
button.getItems().addAll(updateItem, reinstallItem, uninstallItem); 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) { private void trySetup(final SetupManager manager, final SetupConsumer consumer, final String operation) {
try { manager.addListener(this);
consumer.accept(manager); CompletableFuture.runAsync(() -> {
statusMap.get(manager).set(manager.status()); try {
} catch (final SetupException e) { consumer.accept(manager);
logger.error("Error {}ing {}", operation, manager.name(), e); Platform.runLater(() -> {
showErrorDialog(resources.getString("setup." + operation + ".error.title"), MessageFormat.format(resources.getString("setup." + operation + ".error.message"), e.getMessage())); 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 @FunctionalInterface
private interface SetupConsumer { private interface SetupConsumer {
void accept(SetupManager manager) throws SetupException; 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 @Override
public FXSetupModel model() { public FXSetupModel model() {
return model; return model;
} }
public Window window() { @Override
protected Window window() {
return converterNameLabel.getScene().getWindow(); return converterNameLabel.getScene().getWindow();
} }
} }

View File

@@ -39,20 +39,20 @@ public class FXSetupModel implements SetupModel {
@Inject @Inject
FXSetupModel() { FXSetupModel() {
this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED); this.subtitleExtractorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false); this.subtitleExtractorInstalled = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false); this.subtitleExtractorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(0); this.subtitleExtractorSetupProgress = new SimpleDoubleProperty(-2);
this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty(""); this.subtitleExtractorSetupProgressLabel = new SimpleStringProperty("");
this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED); this.videoConverterStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false); this.videoConverterInstalled = new ReadOnlyBooleanWrapper(false);
this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false); this.videoConverterUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.videoConverterSetupProgress = new SimpleDoubleProperty(0); this.videoConverterSetupProgress = new SimpleDoubleProperty(-2);
this.videoConverterSetupProgressLabel = new SimpleStringProperty(""); this.videoConverterSetupProgressLabel = new SimpleStringProperty("");
this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.NOT_INSTALLED); this.translatorStatus = new SimpleObjectProperty<>(SetupStatus.ERRORED);
this.translatorInstalled = new ReadOnlyBooleanWrapper(false); this.translatorInstalled = new ReadOnlyBooleanWrapper(false);
this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false); this.translatorUpdateAvailable = new ReadOnlyBooleanWrapper(false);
this.translatorSetupProgress = new SimpleDoubleProperty(0); this.translatorSetupProgress = new SimpleDoubleProperty(-2);
this.translatorSetupProgressLabel = new SimpleStringProperty(""); this.translatorSetupProgressLabel = new SimpleStringProperty("");
subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus)); subtitleExtractorInstalled.bind(Bindings.createBooleanBinding(() -> subtitleExtractorStatus.get().isInstalled(), subtitleExtractorStatus));

View File

@@ -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());
}
}

View File

@@ -1,38 +1,48 @@
package com.github.gtache.autosubtitle.gui.fx; package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator; import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.VideoConverter; import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader; 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.WorkController;
import com.github.gtache.autosubtitle.gui.WorkStatus;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; 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.SubtitleExtractor;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractorListener;
import com.github.gtache.autosubtitle.subtitle.fx.ObservableSubtitleImpl; 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.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button; 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.TableColumn;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.KeyCode;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Window; import javafx.stage.Window;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.controlsfx.control.CheckComboBox;
import org.controlsfx.control.PrefixSelectionComboBox;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle; 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 java.util.stream.Stream;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@@ -41,14 +51,14 @@ import static java.util.Objects.requireNonNull;
* FX implementation of {@link WorkController} * FX implementation of {@link WorkController}
*/ */
@Singleton @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 Logger logger = LogManager.getLogger(FXWorkController.class);
private static final List<String> VIDEO_EXTENSIONS = List.of(".webm", ".mkv", ".flv", ".vob", ".ogv", ".ogg", private static final List<String> VIDEO_EXTENSIONS = Stream.of("webm", "mkv", "flv", "vob", "ogv", "ogg",
".drc", ".gif", ".gifv", ".mng", ".avi", ".mts", ".m2ts", ".ts", ".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", "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", "viv", "asf", "amv", "mp4", "m4p", "m4v", "mpg", "mp2", "mpeg", "mpe", "mpv", "m2v", "m4v", "svi",
".3gp", ".3g2", ".mxf", ".roq", ".nsv", ".flv", ".f4v", ".f4p", ".f4a", ".f4b"); "3gp", "3g2", "mxf", "roq", "nsv", "flv", "f4v", "f4p", "f4a", "f4b").map(s -> "*." + s).toList();
@FXML @FXML
private TextField fileField; private TextField fileField;
@@ -69,56 +79,125 @@ public class FXWorkController implements WorkController {
@FXML @FXML
private TableColumn<EditableSubtitle, String> textColumn; private TableColumn<EditableSubtitle, String> textColumn;
@FXML @FXML
private TextField translationField;
@FXML
private FXMediaController mediaController; private FXMediaController mediaController;
@FXML
private Button addSubtitleButton;
@FXML
private PrefixSelectionComboBox<Language> languageCombobox;
@FXML
private CheckComboBox<Language> translationsCombobox;
@FXML
private Label progressLabel;
@FXML
private ProgressBar progressBar;
@FXML
private Label progressDetailLabel;
@FXML @FXML
private ResourceBundle resources; private ResourceBundle resources;
private final FXWorkModel model; private final FXWorkModel model;
private final FXWorkBinder binder;
private final SubtitleExtractor subtitleExtractor; private final SubtitleExtractor subtitleExtractor;
private final VideoConverter videoConverter; private final VideoConverter videoConverter;
private final VideoLoader videoLoader; private final VideoLoader videoLoader;
private final Translator translator; private final Translator translator;
private final FXMediaBinder binder; private final TimeFormatter timeFormatter;
@Inject @Inject
FXWorkController(final FXWorkModel model, final SubtitleExtractor subtitleExtractor, final VideoLoader videoLoader, FXWorkController(final FXWorkModel model, final FXWorkBinder binder, final SubtitleExtractor subtitleExtractor, final VideoLoader videoLoader,
final VideoConverter videoConverter, final Translator translator, final FXMediaBinder binder) { final VideoConverter videoConverter, final Translator translator, final TimeFormatter timeFormatter) {
this.model = requireNonNull(model); this.model = requireNonNull(model);
this.binder = requireNonNull(binder);
this.subtitleExtractor = requireNonNull(subtitleExtractor); this.subtitleExtractor = requireNonNull(subtitleExtractor);
this.videoConverter = requireNonNull(videoConverter); this.videoConverter = requireNonNull(videoConverter);
this.videoLoader = requireNonNull(videoLoader); this.videoLoader = requireNonNull(videoLoader);
this.translator = requireNonNull(translator); this.translator = requireNonNull(translator);
this.binder = requireNonNull(binder); this.timeFormatter = requireNonNull(timeFormatter);
} }
@FXML @FXML
private void initialize() { 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())); resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()));
exportSoftButton.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())); exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
addSubtitleButton.disableProperty().bind(model.videoProperty().isNull());
subtitlesTable.setItems(model.subtitles()); 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.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())); 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) -> { model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) { if (newValue != null) {
mediaController.seek(newValue.start()); 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(); binder.createBindings();
subtitleExtractor.addListener(this);
}
private void editFocusedCell() {
final var focusedCell = subtitlesTable.getFocusModel().getFocusedCell();
if (focusedCell != null) {
subtitlesTable.edit(focusedCell.getRow(), focusedCell.getTableColumn());
}
} }
@FXML @FXML
private void fileButtonPressed() { private void fileButtonPressed() {
final var filePicker = new FileChooser(); 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()); final var file = filePicker.showOpenDialog(window());
if (file != null) { if (file != null) {
loadVideo(file.toPath()); loadVideo(file.toPath());
@@ -128,12 +207,32 @@ public class FXWorkController implements WorkController {
@Override @Override
public void extractSubtitles() { public void extractSubtitles() {
if (model.video() != null) { if (model.video() != null) {
final var subtitles = subtitleExtractor.extract(model.video()).stream().sorted(Comparator.comparing(Subtitle::start).thenComparing(Subtitle::end)).toList(); model.setStatus(WorkStatus.EXTRACTING);
final var subtitlesCopy = subtitles.stream().map(ObservableSubtitleImpl::new).toList(); CompletableFuture.supplyAsync(this::extractAsync).whenCompleteAsync(this::manageExtractResult, Platform::runLater);
model.subtitles().setAll(subtitles); }
}
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().clear();
model.originalSubtitles().addAll(subtitlesCopy); 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 @Override
@@ -144,6 +243,7 @@ public class FXWorkController implements WorkController {
model.videoProperty().set(loadedVideo); model.videoProperty().set(loadedVideo);
} catch (final IOException e) { } catch (final IOException e) {
logger.error("Error loading video {}", file, 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 filePicker = new FileChooser();
final var file = filePicker.showSaveDialog(window()); final var file = filePicker.showSaveDialog(window());
if (file != null) { if (file != null) {
final var text = model.subtitles().stream().map(Subtitle::content).collect(Collectors.joining(" ")); final var baseCollection = model.subtitleCollection();
final var baseCollection = new SubtitleCollectionImpl(model.subtitles(), translator.getLocale(text)); final var translations = model.translations();
final var collections = Stream.concat(Stream.of(baseCollection), model.translations().stream().map(l -> translator.translate(baseCollection, l))).toList(); model.setStatus(WorkStatus.TRANSLATING);
try { CompletableFuture.supplyAsync(() -> Stream.concat(Stream.of(baseCollection), translations.stream().map(l -> translator.translate(baseCollection, l))).toList())
videoConverter.addSoftSubtitles(model.video(), collections); .thenApplyAsync(c -> {
} catch (final IOException e) { model.setStatus(WorkStatus.EXPORTING);
logger.error("Error exporting subtitles", e); return c;
final var alert = new Alert(Alert.AlertType.ERROR, MessageFormat.format(resources.getString("work.error.export.label"), e.getMessage()), ButtonType.OK); }, Platform::runLater)
alert.setTitle(resources.getString("work.error.export.title")); .thenAcceptAsync(collections -> {
alert.showAndWait(); 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 filePicker = new FileChooser();
final var file = filePicker.showSaveDialog(window()); final var file = filePicker.showSaveDialog(window());
if (file != null) { if (file != null) {
try { CompletableFuture.runAsync(() -> {
videoConverter.addHardSubtitles(model.video(), new SubtitleCollectionImpl(model.subtitles(), Locale.getDefault())); try {
} catch (final IOException e) { videoConverter.addHardSubtitles(model.video(), model.subtitleCollection());
logger.error("Error exporting subtitles", e); } catch (final IOException e) {
final var alert = new Alert(Alert.AlertType.ERROR, MessageFormat.format(resources.getString("work.error.export.label"), e.getMessage()), ButtonType.OK); throw new CompletionException(e);
alert.setTitle(resources.getString("work.error.export.title")); }
alert.showAndWait(); }).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() { private void resetButtonPressed() {
model.subtitles().setAll(model.originalSubtitles()); 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());
});
}
} }

View File

@@ -1,9 +1,21 @@
package com.github.gtache.autosubtitle.gui.fx; package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video; import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.gui.WorkModel; 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.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.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.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@@ -11,8 +23,10 @@ import javafx.collections.ObservableList;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.stream.Collectors;
/** /**
* FX implementation of {@link WorkModel} * FX implementation of {@link WorkModel}
@@ -21,18 +35,52 @@ import java.util.Locale;
public class FXWorkModel implements WorkModel { public class FXWorkModel implements WorkModel {
private final ObjectProperty<Video> video; private final ObjectProperty<Video> video;
private final ReadOnlyObjectWrapper<SubtitleCollection> subtitleCollection;
private final ObservableList<EditableSubtitle> subtitles; private final ObservableList<EditableSubtitle> subtitles;
private final List<EditableSubtitle> originalSubtitles; private final List<EditableSubtitle> originalSubtitles;
private final ObjectProperty<EditableSubtitle> subtitle; private final ObjectProperty<EditableSubtitle> subtitle;
private final ObservableList<Locale> translations; private final ObservableList<Language> availableVideoLanguages;
private final ObservableList<Language> availableTranslationLanguages;
private final ObjectProperty<ExtractionModel> extractionModel;
private final ObjectProperty<Language> videoLanguage;
private final ObservableList<Language> translations;
private final ReadOnlyStringWrapper text;
private final ObjectProperty<WorkStatus> workStatus;
private final DoubleProperty progress;
@Inject @Inject
FXWorkModel() { FXWorkModel() {
this.video = new SimpleObjectProperty<>(); this.video = new SimpleObjectProperty<>();
this.subtitleCollection = new ReadOnlyObjectWrapper<>();
this.subtitles = FXCollections.observableArrayList(); this.subtitles = FXCollections.observableArrayList();
this.originalSubtitles = new ArrayList<>(); this.originalSubtitles = new ArrayList<>();
this.subtitle = new SimpleObjectProperty<>(); this.subtitle = new SimpleObjectProperty<>();
this.availableVideoLanguages =
FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(Arrays.stream(Language.values())
.sorted((o1, o2) -> {
if (o1 == Language.AUTO) {
return -1;
} else if (o2 == Language.AUTO) {
return 1;
} else {
return o1.compareTo(o2);
}
}).toList()));
this.availableTranslationLanguages =
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO).toList());
this.videoLanguage = new SimpleObjectProperty<>(Language.AUTO);
this.translations = FXCollections.observableArrayList(); this.translations = FXCollections.observableArrayList();
this.text = new ReadOnlyStringWrapper("");
this.workStatus = new SimpleObjectProperty<>(WorkStatus.IDLE);
this.extractionModel = new SimpleObjectProperty<>();
this.progress = new SimpleDoubleProperty(-1);
text.bind(Bindings.createStringBinding(() ->
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining(" ")),
subtitles));
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage())));
videoLanguage.addListener((observable, oldValue, newValue) -> {
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList());
});
} }
@Override @Override
@@ -40,7 +88,30 @@ public class FXWorkModel implements WorkModel {
return video.get(); return video.get();
} }
public ObjectProperty<Video> videoProperty() { @Override
public ExtractionModel extractionModel() {
return extractionModel.get();
}
@Override
public void setExtractionModel(final ExtractionModel model) {
extractionModel.set(model);
}
ObjectProperty<ExtractionModel> extractionModelProperty() {
return extractionModel;
}
@Override
public SubtitleCollection subtitleCollection() {
return subtitleCollection.get();
}
ReadOnlyObjectProperty<SubtitleCollection> subtitleCollectionProperty() {
return subtitleCollection.getReadOnlyProperty();
}
ObjectProperty<Video> videoProperty() {
return video; return video;
} }
@@ -54,17 +125,78 @@ public class FXWorkModel implements WorkModel {
return originalSubtitles; return originalSubtitles;
} }
@Override
public String text() {
return text.get();
}
ReadOnlyStringProperty textProperty() {
return text.getReadOnlyProperty();
}
@Override @Override
public EditableSubtitle selectedSubtitle() { public EditableSubtitle selectedSubtitle() {
return subtitle.get(); return subtitle.get();
} }
ObjectProperty<EditableSubtitle> selectedSubtitleProperty() {
return subtitle;
}
@Override @Override
public ObservableList<Locale> translations() { public ObservableList<Language> availableVideoLanguages() {
return availableVideoLanguages;
}
@Override
public ObservableList<Language> availableTranslationsLanguage() {
return availableTranslationLanguages;
}
@Override
public Language videoLanguage() {
return videoLanguage.get();
}
@Override
public void setVideoLanguage(final Language language) {
videoLanguage.set(language);
}
ObjectProperty<Language> videoLanguageProperty() {
return videoLanguage;
}
@Override
public ObservableList<Language> translations() {
return translations; return translations;
} }
public ObjectProperty<EditableSubtitle> selectedSubtitleProperty() { @Override
return subtitle; public WorkStatus status() {
return workStatus.get();
}
@Override
public void setStatus(final WorkStatus status) {
workStatus.set(status);
}
ObjectProperty<WorkStatus> statusProperty() {
return workStatus;
}
@Override
public double progress() {
return progress.get();
}
@Override
public void setProgress(final double progress) {
this.progress.set(progress);
}
DoubleProperty progressProperty() {
return progress;
} }
} }

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.Language;
import javafx.util.StringConverter;
class LanguageStringConverter extends StringConverter<Language> {
@Override
public String toString(final Language object) {
return object == null ? "" : object.englishName();
}
@Override
public Language fromString(final String string) {
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import javafx.util.StringConverter;
import java.util.Objects;
class TimeStringConverter extends StringConverter<Long> {
private final TimeFormatter timeFormatter;
TimeStringConverter(final TimeFormatter timeFormatter) {
this.timeFormatter = Objects.requireNonNull(timeFormatter);
}
@Override
public String toString(final Long object) {
return object == null ? "" : timeFormatter.format(object);
}
@Override
public Long fromString(final String string) {
return string == null ? 0L : timeFormatter.parse(string);
}
}

View File

@@ -1,11 +1,15 @@
package com.github.gtache.autosubtitle.gui.modules.fx; package com.github.gtache.autosubtitle.modules.gui.fx;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.gui.fx.ColonTimeFormatter;
import com.github.gtache.autosubtitle.gui.fx.FXMainController; import com.github.gtache.autosubtitle.gui.fx.FXMainController;
import com.github.gtache.autosubtitle.gui.fx.FXMediaController; import com.github.gtache.autosubtitle.gui.fx.FXMediaController;
import com.github.gtache.autosubtitle.gui.fx.FXParametersController;
import com.github.gtache.autosubtitle.gui.fx.FXSetupController; import com.github.gtache.autosubtitle.gui.fx.FXSetupController;
import com.github.gtache.autosubtitle.gui.fx.FXWorkController; import com.github.gtache.autosubtitle.gui.fx.FXWorkController;
import com.github.gtache.autosubtitle.modules.gui.Pause; import com.github.gtache.autosubtitle.modules.gui.Pause;
import com.github.gtache.autosubtitle.modules.gui.Play; import com.github.gtache.autosubtitle.modules.gui.Play;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
@@ -14,6 +18,7 @@ import javafx.scene.image.Image;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.prefs.Preferences;
/** /**
* Dagger module for FX * Dagger module for FX
@@ -21,9 +26,12 @@ import java.util.ResourceBundle;
@Module @Module
public abstract class FXModule { public abstract class FXModule {
@Binds
abstract TimeFormatter bindsTimeFormatter(final ColonTimeFormatter formatter);
@Provides @Provides
@Singleton @Singleton
static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController, static FXMLLoader providesFXMLLoader(final FXMainController mainController, final FXSetupController setupController, final FXParametersController parametersController,
final FXWorkController workController, final FXMediaController mediaController, final ResourceBundle bundle) { final FXWorkController workController, final FXMediaController mediaController, final ResourceBundle bundle) {
final var loader = new FXMLLoader(FXModule.class.getResource("/com/github/gtache/autosubtitle/gui/fx/mainView.fxml")); final var loader = new FXMLLoader(FXModule.class.getResource("/com/github/gtache/autosubtitle/gui/fx/mainView.fxml"));
loader.setResources(bundle); loader.setResources(bundle);
@@ -36,6 +44,8 @@ public abstract class FXModule {
return workController; return workController;
} else if (c == FXMediaController.class) { } else if (c == FXMediaController.class) {
return mediaController; return mediaController;
} else if (c == FXParametersController.class) {
return parametersController;
} else { } else {
throw new IllegalArgumentException("Unknown controller " + c); throw new IllegalArgumentException("Unknown controller " + c);
} }
@@ -56,4 +66,10 @@ public abstract class FXModule {
static Image providesPauseImage(@Pause final byte[] pauseImage) { static Image providesPauseImage(@Pause final byte[] pauseImage) {
return new Image(new ByteArrayInputStream(pauseImage)); return new Image(new ByteArrayInputStream(pauseImage));
} }
@Provides
@Singleton
static Preferences providesPreferences() {
return Preferences.userNodeForPackage(FXParametersController.class);
}
} }

View File

@@ -24,7 +24,11 @@ public class ObservableSubtitleImpl implements EditableSubtitle {
private final ObjectProperty<Bounds> location; private final ObjectProperty<Bounds> location;
public ObservableSubtitleImpl() { public ObservableSubtitleImpl() {
this.content = new SimpleStringProperty(""); this("");
}
public ObservableSubtitleImpl(final String content) {
this.content = new SimpleStringProperty(content);
this.start = new SimpleLongProperty(0); this.start = new SimpleLongProperty(0);
this.end = new SimpleLongProperty(0); this.end = new SimpleLongProperty(0);
this.font = new SimpleObjectProperty<>(); this.font = new SimpleObjectProperty<>();

View File

@@ -11,12 +11,16 @@ module com.github.gtache.autosubtitle.fx {
requires transitive javafx.controls; requires transitive javafx.controls;
requires transitive javafx.media; requires transitive javafx.media;
requires transitive javafx.fxml; requires transitive javafx.fxml;
requires org.controlsfx.controls;
requires org.apache.logging.log4j; requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.gui.fx; requires java.desktop;
exports com.github.gtache.autosubtitle.gui.modules.fx; requires transitive java.prefs;
exports com.github.gtache.autosubtitle.gui.fx;
opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml; opens com.github.gtache.autosubtitle.gui.fx to javafx.fxml;
exports com.github.gtache.autosubtitle.modules.gui.fx;
uses MainBundleProvider; uses MainBundleProvider;
uses SetupBundleProvider; uses SetupBundleProvider;
uses WorkBundleProvider; uses WorkBundleProvider;

View File

@@ -2,17 +2,22 @@
<?import javafx.scene.control.Tab?> <?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?> <?import javafx.scene.control.TabPane?>
<TabPane fx:id="tabPane" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
<TabPane fx:id="tabPane" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXMainController"> fx:controller="com.github.gtache.autosubtitle.gui.fx.FXMainController">
<tabs> <tabs>
<Tab closable="false" text="%main.tab.work.label"> <Tab closable="false" text="%main.tab.work.label">
<content> <content>
<fx:include source="workView.fxml" /> <fx:include source="workView.fxml"/>
</content> </content>
</Tab> </Tab>
<Tab closable="false" text="%main.tab.setup.label"> <Tab closable="false" text="%main.tab.setup.label">
<content> <content>
<fx:include source="setupView.fxml" /> <fx:include source="setupView.fxml"/>
</content>
</Tab>
<Tab closable="false" text="%main.tab.parameters.label">
<content>
<fx:include source="parametersView.fxml"/>
</content> </content>
</Tab> </Tab>
</tabs> </tabs>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.*?>
<?import org.controlsfx.control.PrefixSelectionComboBox?>
<GridPane hgap="10.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" vgap="10.0"
xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXParametersController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES"/>
<ColumnConstraints hgrow="SOMETIMES"/>
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<Label text="%parameters.extraction.model.label"/>
<PrefixSelectionComboBox fx:id="extractionModelCombobox" GridPane.columnIndex="1"/>
<PrefixSelectionComboBox fx:id="extractionOutputFormat" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Label text="%parameters.subtitles.output.format" GridPane.rowIndex="1"/>
<Label text="%parameters.subtitles.font.size" GridPane.rowIndex="3"/>
<Label text="%parameters.subtitles.font.family" GridPane.rowIndex="2"/>
<PrefixSelectionComboBox fx:id="fontFamilyCombobox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<TextField fx:id="fontSizeField" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Button mnemonicParsing="false" onAction="#savePressed" text="%parameters.button.save.label"
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Button mnemonicParsing="false" onAction="#resetPressed" text="%parameters.button.reset.label"
GridPane.rowIndex="4"/>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
</GridPane>

View File

@@ -3,7 +3,6 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuButton?> <?import javafx.scene.control.MenuButton?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.ProgressBar?> <?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?> <?import javafx.scene.layout.GridPane?>
@@ -31,24 +30,9 @@
<Label fx:id="extractorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="2" /> <Label fx:id="extractorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="2" />
<Label fx:id="translatorNameLabel" text="Label" GridPane.rowIndex="3" /> <Label fx:id="translatorNameLabel" text="Label" GridPane.rowIndex="3" />
<Label fx:id="translatorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="3" /> <Label fx:id="translatorStatusLabel" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<MenuButton fx:id="converterButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="1"> <MenuButton fx:id="converterButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="1" />
<items> <MenuButton fx:id="extractorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="2" />
<MenuItem mnemonicParsing="false" text="Action 1" /> <MenuButton fx:id="translatorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="3" />
<MenuItem mnemonicParsing="false" text="Action 2" />
</items>
</MenuButton>
<MenuButton fx:id="extractorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="2">
<items>
<MenuItem mnemonicParsing="false" text="Action 1" />
<MenuItem mnemonicParsing="false" text="Action 2" />
</items>
</MenuButton>
<MenuButton fx:id="translatorButton" mnemonicParsing="false" text="%setup.menu.label" GridPane.columnIndex="2" GridPane.rowIndex="3">
<items>
<MenuItem mnemonicParsing="false" text="Action 1" />
<MenuItem mnemonicParsing="false" text="Action 2" />
</items>
</MenuButton>
<Label text="%setup.description.label" GridPane.columnSpan="2147483647" /> <Label text="%setup.description.label" GridPane.columnSpan="2147483647" />
<ProgressBar fx:id="converterProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="1" /> <ProgressBar fx:id="converterProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="1" />
<ProgressBar fx:id="extractorProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="2" /> <ProgressBar fx:id="extractorProgress" prefWidth="200.0" progress="0.0" GridPane.columnIndex="3" GridPane.rowIndex="2" />

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import org.controlsfx.control.CheckComboBox?>
<?import org.controlsfx.control.PrefixSelectionComboBox?>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" <GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController"> fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
<columnConstraints> <columnConstraints>
@@ -15,6 +16,7 @@
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="ALWAYS"/> <RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="SOMETIMES"/> <RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="NEVER"/>
</rowConstraints> </rowConstraints>
<children> <children>
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/> <TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/>
@@ -38,11 +40,12 @@
</Button> </Button>
</children> </children>
</HBox> </HBox>
<TableView fx:id="subtitlesTable" GridPane.rowIndex="1"> <TableView fx:id="subtitlesTable" editable="true" GridPane.rowIndex="1">
<columns> <columns>
<TableColumn fx:id="startColumn" prefWidth="50.0" text="%work.table.column.from.label"/> <TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false"
<TableColumn fx:id="endColumn" prefWidth="50.0" text="%work.table.column.to.label"/> text="%work.table.column.from.label"/>
<TableColumn fx:id="textColumn" prefWidth="75.0" text="%work.table.column.text.label"/> <TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label"/>
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label"/>
</columns> </columns>
<columnResizePolicy> <columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/> <TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
@@ -52,15 +55,21 @@
text="%work.button.reset.label" GridPane.rowIndex="2"/> text="%work.button.reset.label" GridPane.rowIndex="2"/>
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" <fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.rowIndex="1"/> GridPane.rowIndex="1"/>
<Button mnemonicParsing="false" text="+" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/> <Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+"
GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
<HBox alignment="CENTER_LEFT" spacing="10.0"> <HBox alignment="CENTER_LEFT" spacing="10.0">
<children> <children>
<Label text="%work.language.label"/>
<PrefixSelectionComboBox fx:id="languageCombobox"/>
<Label text="%work.translate.label"/> <Label text="%work.translate.label"/>
<TextField fx:id="translationField"> <CheckComboBox fx:id="translationsCombobox"/>
<tooltip> </children>
<Tooltip text="%work.translate.tooltip"/> </HBox>
</tooltip> <Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
</TextField> <HBox spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
<children>
<Label fx:id="progressDetailLabel"/>
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS"/>
</children> </children>
</HBox> </HBox>
</children> </children>

View File

@@ -64,7 +64,7 @@ public interface MediaModel {
void setPosition(long position); void setPosition(long position);
/** /**
* @return The current list of subtitles * @return The current subtitles
*/ */
List<EditableSubtitle> subtitles(); List<EditableSubtitle> subtitles();
} }

View File

@@ -0,0 +1,22 @@
package com.github.gtache.autosubtitle.gui;
/**
* Controller for the parameters view
*/
public interface ParametersController {
/**
* Saves the parameters
*/
void save();
/**
* Resets the parameters
*/
void reset();
/**
* @return The model
*/
ParametersModel model();
}

View File

@@ -0,0 +1,67 @@
package com.github.gtache.autosubtitle.gui;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.OutputFormat;
import java.util.List;
/**
* Model for the media view
*/
public interface ParametersModel {
/**
* @return The available extraction models
*/
List<ExtractionModel> availableExtractionModels();
/**
* @return The current extraction model
*/
ExtractionModel extractionModel();
/**
* @param model The new extraction model
*/
void setExtractionModel(ExtractionModel model);
/**
* @return The available output formats
*/
List<OutputFormat> availableOutputFormats();
/**
* @return The current output format
*/
OutputFormat outputFormat();
/**
* @param format The new output format
*/
void setOutputFormat(OutputFormat format);
/**
* @return The available font families
*/
List<String> availableFontFamilies();
/**
* @return The current font family
*/
String fontFamily();
/**
* @param fontFamily The new font family
*/
void setFontFamily(String fontFamily);
/**
* @return The current font size
*/
int fontSize();
/**
* @param fontSize The new font size
*/
void setFontSize(int fontSize);
}

View File

@@ -0,0 +1,32 @@
package com.github.gtache.autosubtitle.gui;
/**
* Formatter for times
*/
public interface TimeFormatter {
/**
* Formats the given elapsed time and total time into a string
*
* @param elapsed The elapsed time in milliseconds
* @param total The total time in milliseconds
* @return The formatted string
*/
String format(long elapsed, long total);
/**
* Formats the given time into a string
*
* @param millis The time in milliseconds
* @return The formatted string
*/
String format(long millis);
/**
* Parses the given string
*
* @param time The time as a string
* @return The time in milliseconds
*/
long parse(final String time);
}

View File

@@ -1,10 +1,12 @@
package com.github.gtache.autosubtitle.gui; package com.github.gtache.autosubtitle.gui;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video; import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* Model for the main view * Model for the main view
@@ -16,11 +18,31 @@ public interface WorkModel {
*/ */
Video video(); Video video();
/**
* @return The current extraction model
*/
ExtractionModel extractionModel();
/**
* @param model The new extraction model
*/
void setExtractionModel(ExtractionModel model);
/**
* @return The current subtitle collection
*/
SubtitleCollection subtitleCollection();
/** /**
* @return The current list of subtitles * @return The current list of subtitles
*/ */
List<EditableSubtitle> subtitles(); List<EditableSubtitle> subtitles();
/**
* @return The current text
*/
String text();
/** /**
* @return The original extracted subtitles (used to reset) * @return The original extracted subtitles (used to reset)
*/ */
@@ -31,8 +53,48 @@ public interface WorkModel {
*/ */
EditableSubtitle selectedSubtitle(); EditableSubtitle selectedSubtitle();
/**
* @return The list of available video languages
*/
List<Language> availableVideoLanguages();
/**
* @return The list of available translations languages
*/
List<Language> availableTranslationsLanguage();
/**
* @return The video language
*/
Language videoLanguage();
/**
* @param language The video language
*/
void setVideoLanguage(Language language);
/** /**
* @return The list of selected translations * @return The list of selected translations
*/ */
List<Locale> translations(); List<Language> translations();
/**
* @return The current status
*/
WorkStatus status();
/**
* @param status The new status
*/
void setStatus(WorkStatus status);
/**
* @return The current progress
*/
double progress();
/**
* @param progress The new progress
*/
void setProgress(double progress);
} }

View File

@@ -0,0 +1,8 @@
package com.github.gtache.autosubtitle.gui;
/**
* Possible statuses for the work controller
*/
public enum WorkStatus {
IDLE, EXTRACTING, TRANSLATING, EXPORTING
}

View File

@@ -0,0 +1,9 @@
package com.github.gtache.autosubtitle.gui.spi;
import java.util.spi.ResourceBundleProvider;
/**
* Provider for ParametersBundle
*/
public interface ParametersBundleProvider extends ResourceBundleProvider {
}

View File

@@ -0,0 +1,9 @@
package com.github.gtache.autosubtitle.gui.spi;
import java.util.spi.AbstractResourceBundleProvider;
/**
* Implementation of {@link ParametersBundleProvider}
*/
public class ParametersBundleProviderImpl extends AbstractResourceBundleProvider implements ParametersBundleProvider {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.gui;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FontFamily {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.gui;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FontSize {
}

View File

@@ -6,6 +6,7 @@ import dagger.Provides;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
@@ -19,6 +20,7 @@ public class GuiModule {
return new CombinedResourceBundle(ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.MainBundle"), return new CombinedResourceBundle(ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.MainBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.SetupBundle"), ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.SetupBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.WorkBundle"), ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.WorkBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.ParametersBundle"),
ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.MediaBundle")); ResourceBundle.getBundle("com.github.gtache.autosubtitle.gui.MediaBundle"));
} }
@@ -26,10 +28,10 @@ public class GuiModule {
@Singleton @Singleton
@Play @Play
static byte[] providesPlayImage() { static byte[] providesPlayImage() {
try { try (final var in = GuiModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/play_64.png")) {
return GuiModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/play_64.png").readAllBytes(); return in.readAllBytes();
} catch (final IOException e) { } catch (final IOException e) {
throw new RuntimeException(e); throw new UncheckedIOException(e);
} }
} }
@@ -37,10 +39,24 @@ public class GuiModule {
@Singleton @Singleton
@Pause @Pause
static byte[] providesPauseImage() { static byte[] providesPauseImage() {
try { try (final var in = GuiModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/pause_64.png")) {
return GuiModule.class.getResourceAsStream("/com/github/gtache/autosubtitle/gui/pause_64.png").readAllBytes(); return in.readAllBytes();
} catch (final IOException e) { } catch (final IOException e) {
throw new RuntimeException(e); throw new UncheckedIOException(e);
} }
} }
@Provides
@Singleton
@FontFamily
static String providesFontFamily() {
return "Arial";
}
@Provides
@Singleton
@FontSize
static int providesFontSize() {
return 12;
}
} }

View File

@@ -1,5 +1,7 @@
import com.github.gtache.autosubtitle.gui.spi.MainBundleProvider; import com.github.gtache.autosubtitle.gui.spi.MainBundleProvider;
import com.github.gtache.autosubtitle.gui.spi.MainBundleProviderImpl; import com.github.gtache.autosubtitle.gui.spi.MainBundleProviderImpl;
import com.github.gtache.autosubtitle.gui.spi.ParametersBundleProvider;
import com.github.gtache.autosubtitle.gui.spi.ParametersBundleProviderImpl;
import com.github.gtache.autosubtitle.gui.spi.SetupBundleProvider; import com.github.gtache.autosubtitle.gui.spi.SetupBundleProvider;
import com.github.gtache.autosubtitle.gui.spi.SetupBundleProviderImpl; import com.github.gtache.autosubtitle.gui.spi.SetupBundleProviderImpl;
import com.github.gtache.autosubtitle.gui.spi.WorkBundleProvider; import com.github.gtache.autosubtitle.gui.spi.WorkBundleProvider;
@@ -18,6 +20,7 @@ module com.github.gtache.autosubtitle.gui {
exports com.github.gtache.autosubtitle.modules.gui; exports com.github.gtache.autosubtitle.modules.gui;
provides MainBundleProvider with MainBundleProviderImpl; provides MainBundleProvider with MainBundleProviderImpl;
provides ParametersBundleProvider with ParametersBundleProviderImpl;
provides SetupBundleProvider with SetupBundleProviderImpl; provides SetupBundleProvider with SetupBundleProviderImpl;
provides WorkBundleProvider with WorkBundleProviderImpl; provides WorkBundleProvider with WorkBundleProviderImpl;
} }

View File

@@ -1,2 +1,3 @@
main.tab.parameters.label=Parameters
main.tab.setup.label=Setup main.tab.setup.label=Setup
main.tab.work.label=Work main.tab.work.label=Work

View File

@@ -1,2 +1,3 @@
main.tab.parameters.label=Param\u00E8tres
main.tab.setup.label=Installation main.tab.setup.label=Installation
main.tab.work.label=Travail main.tab.work.label=Travail

View File

@@ -0,0 +1,6 @@
parameters.button.reset.label=Reset
parameters.button.save.label=Save
parameters.extraction.model.label=Model to use for subtitle extraction
parameters.subtitles.font.family=Default subtitle font name
parameters.subtitles.font.size=Default subtitle font size
parameters.subtitles.output.format=Output format for the subtitles

View File

@@ -0,0 +1,6 @@
parameters.button.reset.label=R\u00E9initialiser
parameters.button.save.label=Sauvegarder
parameters.extraction.model.label=Mod\u00E8le utilis\u00E9 pour l'extraction des sous-titres
parameters.subtitles.font.family=Police par d\u00E9faut pour les sous-titres
parameters.subtitles.font.size=Taille de la police par d\u00E9faut pour les sous-titres
parameters.subtitles.output.format=Format de sortie pour les sous-titres

View File

@@ -1,4 +1,16 @@
setup.description.label=Status of the various tools used by the app setup.description.label=Status of the various tools used by the app
setup.event.check.end.label=Checked {0}
setup.event.check.start.label=Checking {0}
setup.event.delete.end.label=Deleted {0}
setup.event.delete.start.label=Deleting {0}
setup.event.download.end.label=Downloaded {0}
setup.event.download.start.label=Downloading {0}
setup.event.install.end.label=Installed {0}
setup.event.install.start.label=Installing {0}
setup.event.uninstall.end.label=Uninstalled {0}
setup.event.uninstall.start.label=Uninstalling {0}
setup.event.update.end.label=Updated {0}
setup.event.update.start.label=Updating {0}
setup.install.error.label=An error occurred while installing : {0} setup.install.error.label=An error occurred while installing : {0}
setup.install.error.title=Error installing setup.install.error.title=Error installing
setup.menu.install.label=Install setup.menu.install.label=Install
@@ -8,9 +20,11 @@ setup.menu.uninstall.label=Uninstall
setup.menu.update.label=Update setup.menu.update.label=Update
setup.reinstall.error.label=An error occurred while reinstalling : {0} setup.reinstall.error.label=An error occurred while reinstalling : {0}
setup.reinstall.error.title=Error reinstalling setup.reinstall.error.title=Error reinstalling
setup.status.bundle_installed.label=is installed.
setup.status.errored.label=threw an error. setup.status.errored.label=threw an error.
setup.status.installed.label=is installed. setup.status.installed.label=is installed.
setup.status.not_installed.label=is not installed. setup.status.not_installed.label=is not installed.
setup.status.system_installed.label=is installed (system).
setup.status.update_available.label=has an available update. setup.status.update_available.label=has an available update.
setup.uninstall.error.label=An error occurred while uninstalling : {0} setup.uninstall.error.label=An error occurred while uninstalling : {0}
setup.uninstall.error.title=Error uninstalling setup.uninstall.error.title=Error uninstalling

View File

@@ -1,4 +1,16 @@
setup.description.label=Statut des outils utilis\u00E9s par l'application setup.description.label=Statut des outils utilis\u00E9s par l'application
setup.event.check.end.label=Contr\u00F4le de {0} termin\u00E9
setup.event.check.start.label=Contr\u00F4le de {0} en cours
setup.event.delete.end.label=Suppression de {0} termin\u00E9e
setup.event.delete.start.label=Suppression de {0} en cours
setup.event.download.end.label=T\u00E9l\u00E9chargement de {0} termin\u00E9
setup.event.download.start.label=T\u00E9l\u00E9chargement de {0} en cours
setup.event.install.end.label=Installation de {0} termin\u00E9e
setup.event.install.start.label=Installation de {0} en cours
setup.event.uninstall.end.label=D\u00E9sinstallation de {0} termin\u00E9e
setup.event.uninstall.start.label=D\u00E9sinstallation de {0} en cours
setup.event.update.end.label=Mise \u00E0 jour de {0} termin\u00E9e
setup.event.update.start.label=Mise \u00E0 jour de {0} en cours
setup.install.error.label=Une erreur s''est produite lors de l''installation: {0} setup.install.error.label=Une erreur s''est produite lors de l''installation: {0}
setup.install.error.title=Erreur d'installation setup.install.error.title=Erreur d'installation
setup.menu.install.label=Installer setup.menu.install.label=Installer
@@ -8,9 +20,11 @@ setup.menu.uninstall.label=D\u00E9sinstaller
setup.menu.update.label=Mettre \u00E0 jour setup.menu.update.label=Mettre \u00E0 jour
setup.reinstall.error.label=Une erreur s''est produite lors de la r\u00E9installation: {0} setup.reinstall.error.label=Une erreur s''est produite lors de la r\u00E9installation: {0}
setup.reinstall.error.title=Erreur de r\u00E9installation setup.reinstall.error.title=Erreur de r\u00E9installation
setup.status.bundle_installed.label=est install\u00E9.
setup.status.errored.label=a caus\u00E9 une erreur. setup.status.errored.label=a caus\u00E9 une erreur.
setup.status.installed.label=est install\u00E9. setup.status.installed.label=est install\u00E9.
setup.status.not_installed.label=n'est pas install\u00E9. setup.status.not_installed.label=n'est pas install\u00E9.
setup.status.system_installed.label=est install\u00E9 (syst\u00E8me).
setup.status.update_available.label=a une mise \u00E0 jour disponible. setup.status.update_available.label=a une mise \u00E0 jour disponible.
setup.uninstall.error.label=Une erreur s''est produite lors de la d\u00E9sinstallation : {0} setup.uninstall.error.label=Une erreur s''est produite lors de la d\u00E9sinstallation : {0}
setup.uninstall.error.title=Erreur de d\u00E9sinstallation setup.uninstall.error.title=Erreur de d\u00E9sinstallation

View File

@@ -5,10 +5,18 @@ work.button.export.soft.tooltip=Adds the subtitles to the video. This allows a v
work.button.extract.label=Extract subtitles work.button.extract.label=Extract subtitles
work.button.file.label=Open video... work.button.file.label=Open video...
work.button.reset.label=Reset subtitles work.button.reset.label=Reset subtitles
work.error.export.label=Error during the export : {0} work.export.error.label=Error during the export : {0}
work.error.export.title=Error exporting work.export.error.title=Error exporting
work.extract.error.label=Error extracting subtitles : {0}
work.extract.error.title=Error extracting
work.language.label=Video language
work.load.error.label=Error loading video : {0}
work.load.error.title=Error loading
work.status.exporting.label=Exporting...
work.status.extracting.label=Extracting...
work.status.idle.label=Idle
work.status.translating.label=Translating...
work.table.column.from.label=From work.table.column.from.label=From
work.table.column.text.label=Text work.table.column.text.label=Text
work.table.column.to.label=To work.table.column.to.label=To
work.translate.label=Automatic translation (for adding) work.translate.label=Automatic translations
work.translate.tooltip=A comma-separated list of ISO 639-3 codes

View File

@@ -1,3 +1,22 @@
work.button.export.hard.label=Graver la vid\u00E9o...
work.button.export.hard.tooltip=Grave les sous-titres dans la vid\u00E9o. Cela veut dire que les sous-titres sont \u00E9crits directement dans l'image et ne peuvent pas \u00EAtre d\u00E9sactiv\u00E9s.
work.button.export.soft.label=Exporter la vid\u00E9o...
work.button.export.soft.tooltip=Ajoute les sous-titres \u00E0 la vid\u00E9o. Cela permet d'avoir plusieurs pistes de sous-titres dans une m\u00EAme vid\u00E9o et de les activer comme d\u00E9sir\u00E9.
work.button.extract.label=Extraire les sous-titres
work.button.file.label=Ouvrir une vid\u00E9o...
work.button.reset.label=R\u00E9initialiser les sous-titres
work.export.error.label=Erreur durant l''export : {0}
work.export.error.title=Erreur d'export
work.extract.error.label=Erreur durant l''extraction des sous-titres : {0}
work.extract.error.title=Erreur d'extraction
work.language.label=Language de la vid\u00E9o
work.load.error.label=Erreur lors du chargement de la vid\u00E9o : {0}
work.load.error.title=Erreur de chargement
work.status.exporting.label=Exportation en cours...
work.status.extracting.label=Extraction en cours...
work.status.idle.label=Idle
work.status.translating.label=Traduction en cours...
work.table.column.from.label=De work.table.column.from.label=De
work.table.column.text.label=Texte work.table.column.text.label=Texte
work.table.column.to.label=\u00C0 work.table.column.to.label=\u00C0
work.translate.label=Traductions automatiques

27
pom.xml
View File

@@ -17,6 +17,7 @@
<module>gui</module> <module>gui</module>
<module>run</module> <module>run</module>
<module>whisper</module> <module>whisper</module>
<module>cli</module>
</modules> </modules>
<properties> <properties>
@@ -26,6 +27,7 @@
<dagger.version>2.51.1</dagger.version> <dagger.version>2.51.1</dagger.version>
<log4j.version>2.23.1</log4j.version> <log4j.version>2.23.1</log4j.version>
<picocli.version>4.7.6</picocli.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
@@ -35,6 +37,11 @@
<artifactId>autosubtitle-api</artifactId> <artifactId>autosubtitle-api</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-cli</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.gtache.autosubtitle</groupId> <groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-core</artifactId> <artifactId>autosubtitle-core</artifactId>
@@ -80,6 +87,16 @@
<artifactId>log4j-api</artifactId> <artifactId>log4j-api</artifactId>
<version>${log4j.version}</version> <version>${log4j.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@@ -91,16 +108,22 @@
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version> <version>3.13.0</version>
<configuration> <configuration>
<annotationProcessorPaths> <annotationProcessorPaths combine.children="append">
<path> <path>
<groupId>com.google.dagger</groupId> <groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId> <artifactId>dagger-compiler</artifactId>
<version>${dagger.version}</version> <version>${dagger.version}</version>
</path> </path>
<path>
<groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId>
<version>${picocli.version}</version>
</path>
</annotationProcessorPaths> </annotationProcessorPaths>
<compilerArgs> <compilerArgs combine.children="append">
<arg>-Adagger.fastInit=enabled</arg> <arg>-Adagger.fastInit=enabled</arg>
<arg>-Adagger.ignoreProvisionKeyWildcards=ENABLED</arg> <arg>-Adagger.ignoreProvisionKeyWildcards=ENABLED</arg>
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
<arg>-Xlint:all</arg> <arg>-Xlint:all</arg>
</compilerArgs> </compilerArgs>
<showDeprecation>true</showDeprecation> <showDeprecation>true</showDeprecation>

View File

@@ -27,7 +27,10 @@
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId> <artifactId>log4j-core</artifactId>
<version>${log4j.version}</version> </dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -1,23 +1,15 @@
package com.github.gtache.autosubtitle.modules.run; package com.github.gtache.autosubtitle.modules.run;
import com.github.gtache.autosubtitle.Audio; import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Translator; import com.github.gtache.autosubtitle.Translator;
import com.github.gtache.autosubtitle.Video; import com.github.gtache.autosubtitle.modules.setup.impl.TranslatorSetup;
import com.github.gtache.autosubtitle.setup.SetupManager; import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.modules.impl.SubtitleExtractorSetup;
import com.github.gtache.autosubtitle.setup.modules.impl.TranslatorSetup;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle; import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/** /**
* Module for missing components * Module for missing components
@@ -30,66 +22,27 @@ public abstract class MissingComponentsModule {
static Translator providesTranslator() { static Translator providesTranslator() {
return new Translator() { return new Translator() {
@Override @Override
public Locale getLocale(final String text) { public Language getLanguage(final String text) {
return Locale.getDefault(); return Language.getDefault();
} }
@Override @Override
public String translate(final String text, final Locale to) { public String translate(final String text, final Language to) {
return text; return text;
} }
@Override @Override
public Subtitle translate(final Subtitle subtitle, final Locale to) { public Subtitle translate(final Subtitle subtitle, final Language to) {
return subtitle; return subtitle;
} }
@Override @Override
public SubtitleCollection translate(final SubtitleCollection collection, final Locale to) { public SubtitleCollection translate(final SubtitleCollection collection, final Language to) {
return collection; return collection;
} }
}; };
} }
@Provides
@Singleton
static SubtitleExtractor providesSubtitleExtractor() {
return new SubtitleExtractor() {
@Override
public Collection<? extends EditableSubtitle> extract(final Video in) {
return List.of();
}
@Override
public Collection<? extends EditableSubtitle> extract(final Audio in) {
return List.of();
}
};
}
@Provides
@Singleton
static SubtitleConverter providesSubtitleConverter() {
return new SubtitleConverter() {
@Override
public String convert(final SubtitleCollection collection) {
return "";
}
@Override
public String formatName() {
return "none";
}
};
}
@Provides
@Singleton
@SubtitleExtractorSetup
static SetupManager providesSubtitleExtractorSetupManager() {
return new NoOpSetupManager();
}
@Provides @Provides
@Singleton @Singleton
@TranslatorSetup @TranslatorSetup

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle.modules.run; package com.github.gtache.autosubtitle.modules.run;
import com.github.gtache.autosubtitle.setup.SetupException; 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.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus; import com.github.gtache.autosubtitle.setup.SetupStatus;
@@ -16,6 +17,11 @@ class NoOpSetupManager implements SetupManager {
return SetupStatus.NOT_INSTALLED; return SetupStatus.NOT_INSTALLED;
} }
@Override
public boolean isInstalled() throws SetupException {
return false;
}
@Override @Override
public void install() throws SetupException { public void install() throws SetupException {
@@ -26,8 +32,33 @@ class NoOpSetupManager implements SetupManager {
} }
@Override
public void reinstall() throws SetupException {
}
@Override
public boolean isUpdateAvailable() throws SetupException {
return false;
}
@Override @Override
public void update() throws SetupException { public void update() throws SetupException {
} }
@Override
public void addListener(final SetupListener listener) {
}
@Override
public void removeListener(final SetupListener listener) {
}
@Override
public void removeListeners() {
}
} }

View File

@@ -1,10 +1,13 @@
package com.github.gtache.autosubtitle.modules.run; package com.github.gtache.autosubtitle.modules.run;
import com.github.gtache.autosubtitle.gui.modules.fx.FXModule;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule; import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegModule;
import com.github.gtache.autosubtitle.modules.gui.GuiModule; import com.github.gtache.autosubtitle.modules.gui.GuiModule;
import com.github.gtache.autosubtitle.modules.gui.fx.FXModule;
import com.github.gtache.autosubtitle.modules.impl.CoreModule; import com.github.gtache.autosubtitle.modules.impl.CoreModule;
import com.github.gtache.autosubtitle.setup.modules.ffmpeg.FFmpegSetupModule; import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegSetupModule;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperSetupModule;
import com.github.gtache.autosubtitle.modules.subtitles.impl.ConverterModule;
import com.github.gtache.autosubtitle.modules.whisper.WhisperModule;
import dagger.Component; import dagger.Component;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
@@ -14,7 +17,8 @@ import javax.inject.Singleton;
* Main component * Main component
*/ */
@Singleton @Singleton
@Component(modules = {CoreModule.class, GuiModule.class, FXModule.class, FFmpegModule.class, FFmpegSetupModule.class, MissingComponentsModule.class}) @Component(modules = {CoreModule.class, GuiModule.class, FXModule.class, FFmpegModule.class, FFmpegSetupModule.class,
ConverterModule.class, WhisperModule.class, WhisperSetupModule.class, MissingComponentsModule.class})
public interface RunComponent { public interface RunComponent {
/** /**

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -16,6 +16,11 @@
<groupId>com.github.gtache.autosubtitle</groupId> <groupId>com.github.gtache.autosubtitle</groupId>
<artifactId>autosubtitle-core</artifactId> <artifactId>autosubtitle-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface CondaBundledPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface CondaInstallerPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface CondaMinimumMajorVersion {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.setup.whisper;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface CondaMinimumMinorVersion {
}

Some files were not shown because too many files have changed in this diff Show More