commit 75829244b984d7ff7430549730a9d0096b6cfa3a Author: Guillaume Tâche Date: Sat Jul 27 17:45:46 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e526d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +.idea/sonarlint/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..c2621b8 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..df5f35d --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..6b5c9e1 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..058a36e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,633 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4ae60ba --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..6d50cd4 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..9661ac7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..3e8389c --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.github.gtache.autosubtitle + autosubtitle + 1.0-SNAPSHOT + + + autosubtitle-api + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/api/src/main/java/com/github/gtache/autosubtitle/Audio.java b/api/src/main/java/com/github/gtache/autosubtitle/Audio.java new file mode 100644 index 0000000..81a0abd --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/Audio.java @@ -0,0 +1,21 @@ +package com.github.gtache.autosubtitle; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Represents an audio + */ +public interface Audio { + + /** + * @return The audio input stream + * @throws IOException If an I/O error occurs + */ + InputStream getInputStream() throws IOException; + + /** + * @return The audio info + */ + AudioInfo info(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java b/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java new file mode 100644 index 0000000..68bd2cb --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/AudioInfo.java @@ -0,0 +1,11 @@ +package com.github.gtache.autosubtitle; + +/** + * Represents info about an audio + */ +public interface AudioInfo { + /** + * @return The audio extension (mp3, etc.) + */ + String videoFormat(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/File.java b/api/src/main/java/com/github/gtache/autosubtitle/File.java new file mode 100644 index 0000000..d1e2ba6 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/File.java @@ -0,0 +1,14 @@ +package com.github.gtache.autosubtitle; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public interface File { + default InputStream getInputStream() throws IOException { + return Files.newInputStream(path()); + } + + Path path(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/Translator.java b/api/src/main/java/com/github/gtache/autosubtitle/Translator.java new file mode 100644 index 0000000..b80965a --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/Translator.java @@ -0,0 +1,20 @@ +package com.github.gtache.autosubtitle; + +import com.github.gtache.autosubtitle.subtitle.Subtitle; + +import java.util.Locale; + +public interface Translator { + + String translate(String text, Locale to); + + default String translate(final String text, final String to) { + return translate(text, Locale.forLanguageTag(to)); + } + + Subtitle translate(Subtitle subtitle, Locale to); + + default Subtitle translate(final Subtitle subtitle, final String to) { + return translate(subtitle, Locale.forLanguageTag(to)); + } +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/Video.java b/api/src/main/java/com/github/gtache/autosubtitle/Video.java new file mode 100644 index 0000000..d3a56e9 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/Video.java @@ -0,0 +1,21 @@ +package com.github.gtache.autosubtitle; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Represents a video + */ +public interface Video { + + /** + * @return The video input stream + * @throws IOException If an I/O error occurs + */ + InputStream getInputStream() throws IOException; + + /** + * @return The video info + */ + VideoInfo info(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java b/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java new file mode 100644 index 0000000..f8b8e7e --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/VideoConverter.java @@ -0,0 +1,15 @@ +package com.github.gtache.autosubtitle; + +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; + +import java.io.IOException; +import java.util.Collection; + +public interface VideoConverter { + + Video addSoftSubtitles(final Video video, final Collection subtitles) throws IOException; + + Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException; + + Audio getAudio(final Video video) throws IOException; +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/VideoInfo.java b/api/src/main/java/com/github/gtache/autosubtitle/VideoInfo.java new file mode 100644 index 0000000..b2e940b --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/VideoInfo.java @@ -0,0 +1,11 @@ +package com.github.gtache.autosubtitle; + +/** + * Info about a video + */ +public interface VideoInfo { + /** + * @return The video extension (mp4, etc.) + */ + String videoFormat(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessResult.java b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessResult.java new file mode 100644 index 0000000..af9202a --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessResult.java @@ -0,0 +1,19 @@ +package com.github.gtache.autosubtitle.process; + +import java.util.List; + +/** + * Represents the result of running a process + */ +public interface ProcessResult { + + /** + * @return the exit code of the process + */ + int exitCode(); + + /** + * @return the output of the process + */ + List output(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java new file mode 100644 index 0000000..3321244 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/process/ProcessRunner.java @@ -0,0 +1,31 @@ +package com.github.gtache.autosubtitle.process; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * Runs processes + */ +public interface ProcessRunner { + + /** + * Runs a command + * + * @param args the command + * @return the result + * @throws IOException if something goes wrong + */ + default ProcessResult run(final String... args) throws IOException { + return run(Arrays.asList(args)); + } + + /** + * Runs a command + * + * @param args the command + * @return the result + * @throws IOException if something goes wrong + */ + ProcessResult run(final List args) throws IOException; +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupException.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupException.java new file mode 100644 index 0000000..f6e7236 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupException.java @@ -0,0 +1,16 @@ +package com.github.gtache.autosubtitle.setup; + +public class SetupException extends Exception { + + public SetupException(final String message) { + super(message); + } + + public SetupException(final String message, final Throwable cause) { + super(message, cause); + } + + public SetupException(final Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java new file mode 100644 index 0000000..da4e4de --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupManager.java @@ -0,0 +1,66 @@ +package com.github.gtache.autosubtitle.setup; + +/** + * Manages the setup of a component + */ +public interface SetupManager { + + /** + * @return the name of the component + */ + String name(); + + /** + * @return the status of the component setup + */ + SetupStatus status(); + + /** + * @return whether the component is installed + * @throws SetupException if an error occurred during the check + */ + default boolean isInstalled() throws SetupException { + return status() != SetupStatus.NOT_INSTALLED; + } + + /** + * Installs the component + * + * @throws SetupException if an error occurred during the installation + */ + void install() throws SetupException; + + /** + * Uninstalls the component + * + * @throws SetupException if an error occurred during the uninstallation + */ + void uninstall() throws SetupException; + + /** + * Reinstalls the component + * + * @throws SetupException if an error occurred during the reinstallation + */ + default void reinstall() throws SetupException { + uninstall(); + install(); + } + + /** + * Checks if an update is available for the component + * + * @return whether an update is available + * @throws SetupException if an error occurred during the check + */ + default boolean isUpdateAvailable() throws SetupException { + return status() == SetupStatus.UPDATE_AVAILABLE; + } + + /** + * Updates the component + * + * @throws SetupException if an error occurred during the update + */ + void update() throws SetupException; +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java new file mode 100644 index 0000000..2be94e7 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/setup/SetupStatus.java @@ -0,0 +1,8 @@ +package com.github.gtache.autosubtitle.setup; + +/** + * The status of a setup + */ +public enum SetupStatus { + NOT_INSTALLED, INSTALLED, UPDATE_AVAILABLE +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Bounds.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Bounds.java new file mode 100644 index 0000000..967e853 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Bounds.java @@ -0,0 +1,28 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * Represents the bounds of an object + */ +public interface Bounds { + + /** + * @return the x coordinate + */ + double x(); + + /** + * @return the y coordinate + */ + double y(); + + + /** + * @return the width + */ + double width(); + + /** + * @return the height + */ + double height(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/EditableSubtitle.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/EditableSubtitle.java new file mode 100644 index 0000000..05ab12c --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/EditableSubtitle.java @@ -0,0 +1,42 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * Subtitle that can be edited + */ +public interface EditableSubtitle extends Subtitle { + + /** + * Sets the content of the subtitle + * + * @param content the new content + */ + void setContent(String content); + + /** + * Sets the start time of the subtitle + * + * @param start the new start time (in milliseconds) + */ + void setStart(final long start); + + /** + * Sets the end time of the subtitle + * + * @param end the new end time (in milliseconds) + */ + void setEnd(final long end); + + /** + * Sets the font of the subtitle + * + * @param font the new font + */ + void setFont(final Font font); + + /** + * Sets the location of the subtitle + * + * @param bounds the new location + */ + void setBounds(final Bounds bounds); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Font.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Font.java new file mode 100644 index 0000000..a89ee77 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Font.java @@ -0,0 +1,8 @@ +package com.github.gtache.autosubtitle.subtitle; + +public interface Font { + + String name(); + + int size(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Subtitle.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Subtitle.java new file mode 100644 index 0000000..773f932 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/Subtitle.java @@ -0,0 +1,41 @@ +package com.github.gtache.autosubtitle.subtitle; + +import java.time.Duration; + +/** + * Represents a subtitle + */ +public interface Subtitle { + + /** + * @return the content of the subtitle + */ + String content(); + + /** + * @return the start time of the subtitle in milliseconds + */ + long start(); + + /** + * @return the end time of the subtitle in milliseconds + */ + long end(); + + /** + * @return the duration of the subtitle + */ + default Duration duration() { + return Duration.ofMillis(end() - start()); + } + + /** + * @return the font of the subtitle + */ + Font font(); + + /** + * @return the location and size of the subtitle + */ + Bounds bounds(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java new file mode 100644 index 0000000..cc6232b --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleCollection.java @@ -0,0 +1,19 @@ +package com.github.gtache.autosubtitle.subtitle; + +import java.util.Collection; + +/** + * Represents a collection of {@link Subtitle} + */ +public interface SubtitleCollection { + + /** + * @return The subtitles + */ + Collection subtitles(); + + /** + * @return The language of the subtitles + */ + String language(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleConverter.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleConverter.java new file mode 100644 index 0000000..f705fd2 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleConverter.java @@ -0,0 +1,20 @@ +package com.github.gtache.autosubtitle.subtitle; + +/** + * Converts subtitles to a specific format (e.g. srt, ssa, ass, ...) + */ +public interface SubtitleConverter { + + /** + * Converts the subtitle collection + * + * @param collection The collection + * @return The converted subtitles as the content of a file + */ + String convert(final SubtitleCollection collection); + + /** + * @return The name of the format + */ + String formatName(); +} diff --git a/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java new file mode 100644 index 0000000..b64a532 --- /dev/null +++ b/api/src/main/java/com/github/gtache/autosubtitle/subtitle/SubtitleExtractor.java @@ -0,0 +1,16 @@ +package com.github.gtache.autosubtitle.subtitle; + +import com.github.gtache.autosubtitle.Audio; +import com.github.gtache.autosubtitle.Video; + +import java.util.Collection; + +/** + * Extracts subtitles from a video or audio + */ +public interface SubtitleExtractor { + + Collection extract(final Video in); + + Collection extract(final Audio in); +} diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..87987b4 --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + com.github.gtache.autosubtitle + autosubtitle + 1.0-SNAPSHOT + + + autosubtitle-core + + + + com.github.gtache.autosubtitle + autosubtitle-api + + + com.google.dagger + dagger + + + org.apache.logging.log4j + log4j-api + 2.23.1 + + + \ No newline at end of file diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java new file mode 100644 index 0000000..704af00 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/AudioInfoImpl.java @@ -0,0 +1,20 @@ +package com.github.gtache.autosubtitle.impl; + +import com.github.gtache.autosubtitle.AudioInfo; + +import java.util.Objects; + +/** + * Implementation of {@link AudioInfo} + */ +public record AudioInfoImpl(String audioFormat) implements AudioInfo { + + public AudioInfoImpl { + Objects.requireNonNull(audioFormat); + } + + @Override + public String videoFormat() { + return audioFormat; + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/FileAudioImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/FileAudioImpl.java new file mode 100644 index 0000000..5b184ba --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/FileAudioImpl.java @@ -0,0 +1,27 @@ +package com.github.gtache.autosubtitle.impl; + +import com.github.gtache.autosubtitle.Audio; +import com.github.gtache.autosubtitle.AudioInfo; +import com.github.gtache.autosubtitle.File; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +import static java.util.Objects.requireNonNull; + +/** + * Implementation of {@link Audio} with a {@link File} + */ +public record FileAudioImpl(Path path, AudioInfo info) implements Audio, File { + + public FileAudioImpl { + requireNonNull(path); + requireNonNull(info); + } + + @Override + public InputStream getInputStream() throws IOException { + return File.super.getInputStream(); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/FileVideoImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/FileVideoImpl.java new file mode 100644 index 0000000..bb2e551 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/FileVideoImpl.java @@ -0,0 +1,27 @@ +package com.github.gtache.autosubtitle.impl; + +import com.github.gtache.autosubtitle.File; +import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.VideoInfo; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +import static java.util.Objects.requireNonNull; + +/** + * Implementation of {@link Video} with a {@link File} + */ +public record FileVideoImpl(Path path, VideoInfo info) implements Video, File { + + public FileVideoImpl { + requireNonNull(path); + requireNonNull(info); + } + + @Override + public InputStream getInputStream() throws IOException { + return File.super.getInputStream(); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/MemoryAudioImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/MemoryAudioImpl.java new file mode 100644 index 0000000..8e71601 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/MemoryAudioImpl.java @@ -0,0 +1,25 @@ +package com.github.gtache.autosubtitle.impl; + +import com.github.gtache.autosubtitle.Audio; +import com.github.gtache.autosubtitle.AudioInfo; + +import java.io.InputStream; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +/** + * In-memory implementation of {@link Audio} + */ +public record MemoryAudioImpl(Supplier inputStreamSupplier, AudioInfo info) implements Audio { + + public MemoryAudioImpl { + requireNonNull(inputStreamSupplier); + requireNonNull(info); + } + + @Override + public InputStream getInputStream() { + return inputStreamSupplier.get(); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/MemoryVideoImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/MemoryVideoImpl.java new file mode 100644 index 0000000..5e98cbc --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/MemoryVideoImpl.java @@ -0,0 +1,25 @@ +package com.github.gtache.autosubtitle.impl; + +import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.VideoInfo; + +import java.io.InputStream; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +/** + * In-memory implementation of {@link Video} + */ +public record MemoryVideoImpl(Supplier inputStreamSupplier, VideoInfo info) implements Video { + + public MemoryVideoImpl { + requireNonNull(inputStreamSupplier); + requireNonNull(info); + } + + @Override + public InputStream getInputStream() { + return inputStreamSupplier.get(); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/impl/VideoInfoImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/impl/VideoInfoImpl.java new file mode 100644 index 0000000..99deb0b --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/impl/VideoInfoImpl.java @@ -0,0 +1,20 @@ +package com.github.gtache.autosubtitle.impl; + +import com.github.gtache.autosubtitle.VideoInfo; + +import java.util.Objects; + +/** + * Implementation of {@link VideoInfo} + */ +public record VideoInfoImpl(String videoFormat) implements VideoInfo { + + public VideoInfoImpl { + Objects.requireNonNull(videoFormat); + } + + @Override + public String videoFormat() { + return videoFormat; + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java new file mode 100644 index 0000000..a0e4a1e --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/AbstractProcessRunner.java @@ -0,0 +1,48 @@ +package com.github.gtache.autosubtitle.process.impl; + +import com.github.gtache.autosubtitle.process.ProcessResult; +import com.github.gtache.autosubtitle.process.ProcessRunner; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Base implementation of {@link ProcessRunner} + */ +public abstract class AbstractProcessRunner implements ProcessRunner { + + private static final Logger logger = LogManager.getLogger(AbstractProcessRunner.class); + + @Override + public ProcessResult run(final List args) throws IOException { + final var builder = new ProcessBuilder(args); + builder.inheritIO(); + builder.redirectErrorStream(true); + final var process = builder.start(); + final var output = new ArrayList(); + new Thread(() -> { + try (final var in = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream()), StandardCharsets.UTF_8))) { + while (in.ready()) { + output.add(in.readLine()); + } + } catch (final IOException e) { + logger.error("Error listening to process output of {}", args, e); + } + }).start(); + try { + process.waitFor(1, TimeUnit.HOURS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + process.destroy(); + } + return new ProcessResultImpl(process.exitValue(), output); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessResultImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessResultImpl.java new file mode 100644 index 0000000..b14f5e7 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/process/impl/ProcessResultImpl.java @@ -0,0 +1,15 @@ +package com.github.gtache.autosubtitle.process.impl; + +import com.github.gtache.autosubtitle.process.ProcessResult; + +import java.util.List; + +/** + * Implementation of {@link ProcessResult} + */ +public record ProcessResultImpl(int exitCode, List output) implements ProcessResult { + + public ProcessResultImpl { + output = List.copyOf(output); + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/SubtitleExtractor.java b/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/SubtitleExtractor.java new file mode 100644 index 0000000..831400e --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/SubtitleExtractor.java @@ -0,0 +1,16 @@ +package com.github.gtache.autosubtitle.setup.modules.impl; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD}) +public @interface SubtitleExtractor { +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/Translator.java b/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/Translator.java new file mode 100644 index 0000000..32b4c64 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/Translator.java @@ -0,0 +1,16 @@ +package com.github.gtache.autosubtitle.setup.modules.impl; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD}) +public @interface Translator { +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/VideoConverter.java b/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/VideoConverter.java new file mode 100644 index 0000000..d23a279 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/setup/modules/impl/VideoConverter.java @@ -0,0 +1,16 @@ +package com.github.gtache.autosubtitle.setup.modules.impl; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD}) +public @interface VideoConverter { +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/BoundsImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/BoundsImpl.java new file mode 100644 index 0000000..1fb9c71 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/BoundsImpl.java @@ -0,0 +1,24 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.Bounds; + +/** + * Implementation of {@link Bounds} + */ +public record BoundsImpl(double x, double y, double width, double height) implements Bounds { + + public BoundsImpl { + if (x < 0) { + throw new IllegalArgumentException("x must be >= 0 : " + x); + } + if (y < 0) { + throw new IllegalArgumentException("y must be >= 0 : " + y); + } + if (width < 0) { + throw new IllegalArgumentException("width must be >= 0 : " + width); + } + if (height < 0) { + throw new IllegalArgumentException("height must be >= 0 : " + height); + } + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/FontImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/FontImpl.java new file mode 100644 index 0000000..a9e03f5 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/FontImpl.java @@ -0,0 +1,20 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.Font; + +import java.util.Objects; + +/** + * Implementation of {@link Font} + * @param name + * @param size + */ +public record FontImpl(String name, int size) implements Font { + + public FontImpl { + Objects.requireNonNull(name); + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than 0 : " + size); + } + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java new file mode 100644 index 0000000..a599d69 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SRTSubtitleConverter.java @@ -0,0 +1,19 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; +import com.github.gtache.autosubtitle.subtitle.SubtitleConverter; + +/** + * Converts subtitles to SRT format + */ +public class SRTSubtitleConverter implements SubtitleConverter { + + public String convert(final SubtitleCollection collection) { + + } + + @Override + public String formatName() { + return "srt"; + } +} diff --git a/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java new file mode 100644 index 0000000..7295046 --- /dev/null +++ b/core/src/main/java/com/github/gtache/autosubtitle/subtitle/impl/SubtitleCollectionImpl.java @@ -0,0 +1,21 @@ +package com.github.gtache.autosubtitle.subtitle.impl; + +import com.github.gtache.autosubtitle.subtitle.Subtitle; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; + +import java.util.Collection; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * Implementation of {@link SubtitleCollection} + */ +public record SubtitleCollectionImpl(Collection subtitles, + String language) implements SubtitleCollection { + + public SubtitleCollectionImpl { + subtitles = List.copyOf(subtitles); + requireNonNull(language); + } +} diff --git a/ffmpeg/pom.xml b/ffmpeg/pom.xml new file mode 100644 index 0000000..008b641 --- /dev/null +++ b/ffmpeg/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + com.github.gtache.autosubtitle + autosubtitle + 1.0-SNAPSHOT + + + autosubtitle-ffmpeg + + + + com.github.gtache.autosubtitle + autosubtitle-core + + + + \ No newline at end of file diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java new file mode 100644 index 0000000..baf8a9d --- /dev/null +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/ffmpeg/FFmpegVideoConverter.java @@ -0,0 +1,170 @@ +package com.github.gtache.autosubtitle.ffmpeg; + +import com.github.gtache.autosubtitle.Audio; +import com.github.gtache.autosubtitle.File; +import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.VideoConverter; +import com.github.gtache.autosubtitle.impl.AudioInfoImpl; +import com.github.gtache.autosubtitle.impl.FileAudioImpl; +import com.github.gtache.autosubtitle.impl.FileVideoImpl; +import com.github.gtache.autosubtitle.impl.VideoInfoImpl; +import com.github.gtache.autosubtitle.subtitle.SubtitleCollection; +import com.github.gtache.autosubtitle.subtitle.SubtitleConverter; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.util.Objects.requireNonNull; + +/** + * FFmpeg implementation of {@link VideoConverter} + */ +public class FFmpegVideoConverter implements VideoConverter { + + private static final String TEMP_FILE_PREFIX = "autosubtitle"; + private final Path ffmpegPath; + private final SubtitleConverter subtitleConverter; + + @Inject + FFmpegVideoConverter(final Path ffmpegPath, final SubtitleConverter subtitleConverter) { + this.ffmpegPath = requireNonNull(ffmpegPath); + this.subtitleConverter = requireNonNull(subtitleConverter); + } + + @Override + public Video addSoftSubtitles(final Video video, final Collection subtitles) throws IOException { + final var videoPath = getPath(video); + final var collectionMap = dumpCollections(subtitles); + final var out = getTempFile("mkv"); //Soft subtitles are only supported by mkv apparently + final var args = new ArrayList(); + args.add(ffmpegPath.toString()); + args.add("-i"); + args.add(videoPath.toString()); + collectionMap.forEach((c, p) -> { + args.add("-i"); + args.add(p.toString()); + }); + args.add("-c"); + args.add("copy"); + args.add("-map"); + args.add("0:v"); + args.add("-map"); + args.add("0:a"); + final var i = new AtomicInteger(1); + collectionMap.forEach((c, p) -> { + final var n = i.getAndIncrement(); + args.add("-map"); + args.add(String.valueOf(n)); + args.add("-metadata:s:s:" + n); + args.add("language=" + c.language()); + }); + args.add(out.toString()); + run(args); + return new FileVideoImpl(out, new VideoInfoImpl("mkv")); + } + + @Override + public Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException { + final var videoPath = getPath(video); + final var subtitlesPath = dumpSubtitles(subtitles); + final var out = getTempFile(video.info().videoFormat()); + final var subtitleArg = subtitleConverter.formatName().equalsIgnoreCase("ass") ? "ass=" + subtitlesPath : "subtitles=" + subtitlesPath; + final var args = List.of( + ffmpegPath.toString(), + "-i", + videoPath.toString(), + "-vf", + subtitleArg, + out.toString() + ); + run(args); + return new FileVideoImpl(out, video.info()); + } + + @Override + public Audio getAudio(final Video video) throws IOException { + final var videoPath = getPath(video); + final var audioPath = getTempFile(".wav"); + final var dumpVideoPath = getTempFile("." + video.info().videoFormat()); + final var args = List.of( + ffmpegPath.toString(), + "-i", + videoPath.toString(), + "-map", + "0:a", + audioPath.toString(), + "-map", + "0:v", + dumpVideoPath.toString() + ); + run(args); + Files.deleteIfExists(dumpVideoPath); + return new FileAudioImpl(audioPath, new AudioInfoImpl("wav")); + } + + private static Path getPath(final Video video) throws IOException { + if (video instanceof final File f) { + return f.path(); + } else { + return dumpVideo(video); + } + } + + private static Path dumpVideo(final Video video) throws IOException { + final var path = getTempFile(video.info().videoFormat()); + try (final var out = Files.newOutputStream(path)) { + video.getInputStream().transferTo(out); + } + return path; + } + + private SequencedMap dumpCollections(final Collection collections) throws IOException { + final var ret = new LinkedHashMap(collections.size()); + for (final var subtitles : collections) { + ret.put(subtitles, dumpSubtitles(subtitles)); + } + return ret; + } + + private Path dumpSubtitles(final SubtitleCollection subtitles) throws IOException { + final var path = getTempFile("ass"); + Files.writeString(path, subtitleConverter.convert(subtitles)); + return path; + } + + private static Path getTempFile(final String extension) throws IOException { + final var path = Files.createTempFile(TEMP_FILE_PREFIX, "." + extension); + path.toFile().deleteOnExit(); + return path; + } + + private void run(final String... args) throws IOException { + run(Arrays.asList(args)); + } + + private void run(final List args) throws IOException { + final var builder = new ProcessBuilder(args); + builder.inheritIO(); + builder.redirectErrorStream(true); + final var process = builder.start(); + try { + process.waitFor(1, TimeUnit.HOURS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + process.destroy(); + } + if (process.exitValue() != 0) { + throw new IOException("FFmpeg exited with code " + process.exitValue()); + } + } +} diff --git a/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java new file mode 100644 index 0000000..b115e42 --- /dev/null +++ b/ffmpeg/src/main/java/com/github/gtache/autosubtitle/setup/ffmpeg/FFmpegSetupManager.java @@ -0,0 +1,42 @@ +package com.github.gtache.autosubtitle.setup.ffmpeg; + +import com.github.gtache.autosubtitle.setup.SetupException; +import com.github.gtache.autosubtitle.setup.SetupManager; + +/** + * Manager managing the FFmpeg installation + */ +public class FFmpegSetupManager implements SetupManager { + @Override + public boolean isInstalled() throws SetupException { + return checkSystemFFmpeg() || checkBundledFFmpeg(); + } + + @Override + public void install() throws SetupException { + + } + + @Override + public void uninstall() throws SetupException { + + } + + @Override + public boolean isUpdateAvailable() throws SetupException { + return false; + } + + @Override + public void update() throws SetupException { + + } + + private boolean checkSystemFFmpeg() { + return false; + } + + private boolean checkBundledFFmpeg() { + return false; + } +} diff --git a/fx/pom.xml b/fx/pom.xml new file mode 100644 index 0000000..6b07449 --- /dev/null +++ b/fx/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + com.github.gtache.autosubtitle + autosubtitle + 1.0-SNAPSHOT + + + autosubtitle-fx + + + + com.github.gtache.autosubtitle + autosubtitle-gui + + + com.github.gtache.autosubtitle + autosubtitle-core + + + com.google.dagger + dagger + + + org.openjfx + javafx-media + 22.0.1 + + + org.openjfx + javafx-fxml + 22.0.1 + + + + \ No newline at end of file diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java new file mode 100644 index 0000000..9c1a469 --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainController.java @@ -0,0 +1,146 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; +import com.github.gtache.autosubtitle.subtitle.Subtitle; +import com.github.gtache.autosubtitle.subtitle.SubtitleExtractor; +import com.github.gtache.autosubtitle.VideoConverter; +import com.github.gtache.autosubtitle.gui.MainController; +import com.github.gtache.autosubtitle.impl.FileVideoImpl; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.StackPane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaView; +import javafx.stage.FileChooser; +import javafx.stage.Window; + +import javax.inject.Inject; +import java.nio.file.Path; +import java.time.Duration; +import java.time.LocalTime; +import java.util.Comparator; + +import static java.util.Objects.requireNonNull; + +/** + * FX implementation of {@link MainController} + */ +public class FXMainController implements MainController { + + @FXML + private MediaView videoView; + @FXML + private TextField fileField; + @FXML + private Button extractButton; + @FXML + private Button resetButton; + @FXML + private Button exportButton; + @FXML + private TableView subtitlesTable; + @FXML + private TableColumn startColumn; + @FXML + private TableColumn endColumn; + @FXML + private TableColumn textColumn; + @FXML + private StackPane stackPane; + + private final FXMainModel model; + private final SubtitleExtractor subtitleExtractor; + private final VideoConverter videoConverter; + + @Inject + FXMainController(final FXMainModel model, final SubtitleExtractor subtitleExtractor, final VideoConverter videoConverter) { + this.model = requireNonNull(model); + this.subtitleExtractor = requireNonNull(subtitleExtractor); + this.videoConverter = requireNonNull(videoConverter); + } + + @FXML + private void initialize() { + extractButton.disableProperty().bind(model.videoProperty().isNull()); + resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles())); + exportButton.disableProperty().bind(Bindings.isEmpty(model.subtitles())); + + subtitlesTable.setItems(model.subtitles()); + startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start())); + endColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().end())); + textColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? "" : param.getValue().content())); + + model.selectedSubtitleProperty().addListener(new ChangeListener() { + @Override + public void changed(final ObservableValue observable, final EditableSubtitle oldValue, final EditableSubtitle newValue) { + if (newValue != null) { + videoView.getMediaPlayer().seek(Duration.of(newValue.start().to)); + } + } + }); + } + + @FXML + private void fileButtonPressed() { + final var filePicker = new FileChooser(); + final var file = filePicker.showOpenDialog(window()); + if (file != null) { + loadVideo(file.toPath()); + } + } + + @Override + public void extractSubtitles() { + if (model.video() != null) { + final var subtitles = subtitleExtractor.extract(model.video()); + model.subtitles().setAll(subtitles.stream().sorted(Comparator.comparing(Subtitle::start)).toList()); + } + } + + @Override + public void loadVideo(final Path file) { + fileField.setText(file.toAbsolutePath().toString()); + model.videoProperty().set(new FileVideoImpl(file)); + final var media = new Media(file.toUri().toString()); + final var player = new MediaPlayer(media); + videoView.getMediaPlayer().dispose(); + videoView.setMediaPlayer(player); + } + + @Override + public FXMainModel model() { + return model; + } + + public Window window() { + return videoView.getScene().getWindow(); + } + + @FXML + private void extractPressed(final ActionEvent actionEvent) { + extractSubtitles(); + } + + @FXML + private void exportPressed(final ActionEvent actionEvent) { + final var filePicker = new FileChooser(); + final var file = filePicker.showSaveDialog(window()); + if (file != null) { + videoConverter.addSoftSubtitles(model.video(), model.subtitles()); + } + } + + @FXML + private void resetButtonPressed(final ActionEvent actionEvent) { + + } +} diff --git a/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java new file mode 100644 index 0000000..0da134b --- /dev/null +++ b/fx/src/main/java/com/github/gtache/autosubtitle/gui/fx/FXMainModel.java @@ -0,0 +1,51 @@ +package com.github.gtache.autosubtitle.gui.fx; + +import com.github.gtache.autosubtitle.subtitle.EditableSubtitle; +import com.github.gtache.autosubtitle.Video; +import com.github.gtache.autosubtitle.gui.MainModel; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import javax.inject.Inject; + +/** + * FX implementation of {@link MainModel} + */ +public class FXMainModel implements MainModel { + + private final ObjectProperty