commit 114586f5ffda37f292c5630ea6b74ecee5f758aa Author: aNNiMON Date: Tue Jan 10 16:55:02 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87ca788 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.idea/* +.gradle +build/ +out +gen +*.iml \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..a7852d0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '7.0.0' + id 'java' + id 'application' +} + +mainClassName = 'com.annimon.ffmpegbot.Main' +group 'com.annimon' +version '1.0-SNAPSHOT' + +compileJava.options.encoding = 'UTF-8' + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.annimon:tgbots-module:6.3.1' + implementation 'org.slf4j:slf4j-simple:2.0.5' +} + +test { + useJUnitPlatform() +} + +shadowJar { + mergeServiceFiles() + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' +} \ No newline at end of file diff --git a/ffmpegbot.yaml.template b/ffmpegbot.yaml.template new file mode 100644 index 0000000..076edec --- /dev/null +++ b/ffmpegbot.yaml.template @@ -0,0 +1,5 @@ +# Telegram bot token and bot username +botToken: 1234567890:AAAABBBBCCCCDDDDEEEEFF-GGGGHHHHIIII +botUsername: yourbotname +# Allowed user ids +allowedUsers: [12345, 12346, 12347] \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..2d694bd --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'ffmpegbot' diff --git a/src/main/java/com/annimon/ffmpegbot/BotConfig.java b/src/main/java/com/annimon/ffmpegbot/BotConfig.java new file mode 100644 index 0000000..a659aa6 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/BotConfig.java @@ -0,0 +1,10 @@ +package com.annimon.ffmpegbot; + +import java.util.Set; + +public record BotConfig(String botToken, String botUsername, Set allowedUsers) { + + boolean isUserAllowed(Long userId) { + return allowedUsers().contains(userId); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/Main.java b/src/main/java/com/annimon/ffmpegbot/Main.java new file mode 100644 index 0000000..8c3ca83 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/Main.java @@ -0,0 +1,25 @@ +package com.annimon.ffmpegbot; + +import com.annimon.tgbotsmodule.BotHandler; +import com.annimon.tgbotsmodule.BotModule; +import com.annimon.tgbotsmodule.Runner; +import com.annimon.tgbotsmodule.beans.Config; +import com.annimon.tgbotsmodule.services.YamlConfigLoaderService; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class Main implements BotModule { + public static void main(String[] args) { + final var profile = (args.length >= 1 && !args[0].isEmpty()) ? args[0] : ""; + Runner.run(profile, List.of(new Main())); + } + + @Override + public @NotNull BotHandler botHandler(@NotNull Config config) { + final var configLoader = new YamlConfigLoaderService(); + final var configFile = configLoader.configFile("ffmpegbot", config.getProfile()); + final var wordlyConfig = configLoader.loadFile(configFile, BotConfig.class); + return new MainBotHandler(wordlyConfig); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java b/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java new file mode 100644 index 0000000..ac59c7c --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java @@ -0,0 +1,53 @@ +package com.annimon.ffmpegbot; + +import com.annimon.ffmpegbot.commands.HelpCommand; +import com.annimon.ffmpegbot.commands.admin.AdminCommandBundle; +import com.annimon.ffmpegbot.commands.ffmpeg.InputParametersBundle; +import com.annimon.ffmpegbot.commands.ffmpeg.MediaProcessingBundle; +import com.annimon.ffmpegbot.commands.ytdlp.YtDlpCommandBundle; +import com.annimon.ffmpegbot.session.Sessions; +import com.annimon.tgbotsmodule.BotHandler; +import com.annimon.tgbotsmodule.commands.CommandRegistry; +import com.annimon.tgbotsmodule.commands.authority.For; +import org.jetbrains.annotations.NotNull; +import org.telegram.telegrambots.meta.api.methods.BotApiMethod; +import org.telegram.telegrambots.meta.api.objects.Update; + +public class MainBotHandler extends BotHandler { + private final BotConfig botConfig; + private final CommandRegistry commands; + private final MediaProcessingBundle mediaProcessingBundle; + + public MainBotHandler(BotConfig botConfig) { + this.botConfig = botConfig; + commands = new CommandRegistry<>(this, ((update, user, fors) -> botConfig.isUserAllowed(user.getId()))); + final var sessions = new Sessions(); + mediaProcessingBundle = new MediaProcessingBundle(sessions); + commands.registerBundle(mediaProcessingBundle); + commands.registerBundle(new InputParametersBundle(sessions)); + commands.registerBundle(new YtDlpCommandBundle()); + commands.registerBundle(new AdminCommandBundle()); + commands.register(new HelpCommand()); + } + + @Override + protected BotApiMethod onUpdate(@NotNull Update update) { + if (commands.handleUpdate(update)) { + return null; + } + if (update.hasMessage()) { + mediaProcessingBundle.handleMessage(this, update.getMessage()); + } + return null; + } + + @Override + public String getBotUsername() { + return botConfig.botUsername(); + } + + @Override + public String getBotToken() { + return botConfig.botToken(); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/TextUtils.java b/src/main/java/com/annimon/ffmpegbot/TextUtils.java new file mode 100644 index 0000000..03c9ad5 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/TextUtils.java @@ -0,0 +1,19 @@ +package com.annimon.ffmpegbot; + +import java.text.DecimalFormat; + +public class TextUtils { + + public static String safeHtml(String text) { + if (text == null) return ""; + return text.replace("&", "&").replace("<", "<").replace(">" ,">"); + } + + public static String readableFileSize(long size) { + if (size <= 0) return "0"; + final String[] units = new String[] { "Bi", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; + int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); + return new DecimalFormat("#,##0.#") + .format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java b/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java new file mode 100644 index 0000000..f8a03e5 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/HelpCommand.java @@ -0,0 +1,40 @@ +package com.annimon.ffmpegbot.commands; + +import com.annimon.tgbotsmodule.commands.TextCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.MessageContext; +import org.jetbrains.annotations.NotNull; + +import java.util.EnumSet; + +public class HelpCommand implements TextCommand { + + @Override + public String command() { + return "/help"; + } + + @SuppressWarnings("unchecked") + @Override + public EnumSet authority() { + return For.all(); + } + + @Override + public void accept(@NotNull MessageContext ctx) { + ctx.replyToMessage(""" + Media processing + Send any media to start processing. + + Input parameters (in reply to media processing message) + /ss — set media start position + /to — set media end position + /t — set media duration + + yt-dlp + /dl link [format] — download a media using yt-dlp + link — a link to download (it must be supported by yt-dlp) + format — (optional) a download format. Can be "audio", "240", "360", "480", "720" or "1080" + """.stripIndent()).enableHtml().callAsync(ctx.sender); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java new file mode 100644 index 0000000..e52568e --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/admin/AdminCommandBundle.java @@ -0,0 +1,14 @@ +package com.annimon.ffmpegbot.commands.admin; + +import com.annimon.tgbotsmodule.commands.CommandBundle; +import com.annimon.tgbotsmodule.commands.CommandRegistry; +import com.annimon.tgbotsmodule.commands.authority.For; +import org.jetbrains.annotations.NotNull; + +public class AdminCommandBundle implements CommandBundle { + + @Override + public void register(@NotNull CommandRegistry commands) { + commands.register(new RunCommand()); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/admin/RunCommand.java b/src/main/java/com/annimon/ffmpegbot/commands/admin/RunCommand.java new file mode 100644 index 0000000..83006a4 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/admin/RunCommand.java @@ -0,0 +1,78 @@ +package com.annimon.ffmpegbot.commands.admin; + +import com.annimon.tgbotsmodule.commands.TextCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.MessageContext; +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +public class RunCommand implements TextCommand { + + @Override + public String command() { + return "/run"; + } + + @SuppressWarnings("unchecked") + @Override + public EnumSet authority() { + return EnumSet.of(For.CREATOR); + } + + @Override + public void accept(@NotNull MessageContext ctx) { + final String command = ctx.argumentsAsString(); + runCommand(ctx, command); + } + + private void runCommand(@NotNull final MessageContext ctx, @NotNull final String command) { + if (command.isBlank()) return; + CompletableFuture.completedFuture(command) + .thenApplyAsync(this::run) + .handleAsync((output, throwable) -> { + final String text; + if (throwable != null) { + text = "Error " + throwable.getMessage(); + } else if (output.isBlank()) { + text = "Empty output"; + } else { + text = output; + } + return ctx.replyToMessage(text).call(ctx.sender); + }); + } + + private String run(String command) { + try { + final ProcessBuilder pb = new ProcessBuilder(toCommands(command)); + pb.redirectErrorStream(true); + final Process process = pb.start(); + final String output; + try (final var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + output = IOUtils.toString(reader); + } + process.waitFor(); + return output; + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + + private static List toCommands(String command) { + final var commands = new ArrayList(); + final var m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(command); + while (m.find()) { + commands.add(m.group(1)); + } + return commands; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java new file mode 100644 index 0000000..8d75286 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java @@ -0,0 +1,10 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +public final class CallbackQueryCommands { + public static final String PREV = "prev"; + public static final String NEXT = "next"; + public static final String DETAIL = "detail"; + public static final String PROCESS = "process"; + + private CallbackQueryCommands() { } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java new file mode 100644 index 0000000..66f1b48 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java @@ -0,0 +1,169 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +import com.annimon.ffmpegbot.parameters.*; +import com.annimon.ffmpegbot.session.FilePath; +import com.annimon.ffmpegbot.session.FileType; +import com.annimon.ffmpegbot.session.FileTypes; +import com.annimon.ffmpegbot.session.MediaSession; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class FFmpegCommandBuilder implements Visitor { + + private boolean discardAudio; + private final List audioCommands; + private final List videoCommands; + private final List audioFilters; + private final List videoFilters; + + public FFmpegCommandBuilder() { + audioCommands = new ArrayList<>(); + videoCommands = new ArrayList<>(); + audioFilters = new ArrayList<>(); + videoFilters = new ArrayList<>(); + } + + @Override + public void visit(DisableAudio p, MediaSession session) { + discardAudio = p.getValue(); + if (discardAudio) { + audioCommands.add("-an"); + } + } + + @Override + public void visit(AudioBitrate p, MediaSession session) { + if (discardAudio) return; + if (p.getValue().isEmpty()) return; + audioCommands.add("-b:a"); + audioCommands.add(p.getValue()); + } + + @Override + public void visit(AudioEffect p, MediaSession input) { + if (discardAudio) return; + if (p.getValue().isEmpty()) return; + audioFilters.add(switch (p.getValue()) { + case AudioEffect.ECHO -> "aecho=0.8:0.9:40|50|70:0.4|0.3|0.2"; + case AudioEffect.ECHO_2 -> "aecho=0.8:0.9:500|1000:0.2|0.1"; + case AudioEffect.PULSATOR -> "apulsator=mode=sine:hz=0.5"; + case AudioEffect.VIBRATO -> "vibrato=f=4"; + default /* AudioEffect.ROBOT */ -> "afftfilt=\"" + + "real='hypot(re,im)*sin(0)'" + + ":imag='hypot(re,im)*cos(0)'" + + ":win_size=512:overlap=0.75\""; + }); + } + + @Override + public void visit(AudioPitch p, MediaSession input) { + if (discardAudio) return; + if (p.getValue().equals("1")) return; + audioFilters.add("rubberband=pitchq=quality:pitch=" + p.getValue()); + } + + @Override + public void visit(AudioVolume p, MediaSession session) { + if (discardAudio) return; + if (p.getValue().isEmpty()) return; + audioFilters.add("volume=" + p.getValue()); + } + + @Override + public void visit(SpeedFactor p, MediaSession input) { + if (p.getValue().equals("1")) return; + if (!discardAudio) { + audioFilters.add("atempo=" + p.getValue()); + } + videoFilters.add("setpts=PTS/" + p.getValue()); + } + + @Override + public void visit(VideoBitrate p, MediaSession session) { + if (p.getValue().isEmpty()) return; + videoCommands.add("-b:v"); + videoCommands.add(p.getValue()); + } + + @Override + public void visit(VideoScale p, MediaSession session) { + if (p.getValue().isEmpty()) return; + videoFilters.add("scale=-2:" + p.getValue()); + } + + @Override + public void visit(VideoFrameRate p, MediaSession input) { + if (p.getValue().isEmpty()) return; + videoFilters.add("fps=" + p.getValue()); + } + + @Override + public void visit(OutputFormat p, MediaSession session) { + final String localFilename = session.getInputFile().getName(); + String additionalExtension = ""; + + switch (p.getValue()) { + case OutputFormat.VIDEO -> { + session.setFileType(FileType.VIDEO); + additionalExtension = ".mp4"; + } + case OutputFormat.VIDEO_NOTE -> { + session.setFileType(FileType.VIDEO_NOTE); + additionalExtension = ".mp4"; + } + case OutputFormat.AUDIO -> { + session.setFileType(FileType.AUDIO); + additionalExtension = ".mp3"; + } + } + + if (localFilename.toLowerCase(Locale.ENGLISH).endsWith(additionalExtension)) { + additionalExtension = ""; + } + session.setOutputFile(FilePath.outputFile(localFilename + additionalExtension)); + } + + public String[] buildCommand(final @NotNull MediaSession session) { + final var commands = new ArrayList(); + commands.addAll(List.of("ffmpeg", "-loglevel", "quiet", "-stats")); + commands.addAll(buildTrim(session)); + commands.addAll(List.of("-i", FilePath.inputDir() + "/" + session.getInputFile().getName())); + if (FileTypes.canContainAudio(session.getFileType())) { + commands.addAll(audioCommands); + if (!audioFilters.isEmpty()) { + commands.add("-af"); + commands.add(String.join(",", audioFilters)); + } + } + if (FileTypes.canContainVideo(session.getFileType())) { + commands.addAll(videoCommands); + if (!videoFilters.isEmpty()) { + commands.add("-vf"); + commands.add(String.join(",", videoFilters)); + } + } + commands.addAll(List.of("-y", FilePath.outputDir() + "/" + session.getOutputFile().getName())); + return commands.toArray(String[]::new); + } + + private List buildTrim(@NotNull MediaSession session) { + final var commands = new ArrayList(); + final var inputParams = session.getInputParams(); + if (!inputParams.getStartPosition().isEmpty()) { + commands.add("-ss"); + commands.add(inputParams.getStartPosition()); + } + if (!inputParams.getEndPosition().isEmpty()) { + commands.add("-to"); + commands.add(inputParams.getEndPosition()); + } + if (!inputParams.getDuration().isEmpty()) { + commands.add("-t"); + commands.add(inputParams.getDuration()); + } + return commands; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegTask.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegTask.java new file mode 100644 index 0000000..70f9f9f --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegTask.java @@ -0,0 +1,34 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +import com.annimon.ffmpegbot.parameters.Parameter; +import com.annimon.ffmpegbot.session.MediaSession; + +import java.io.IOException; +import java.util.Scanner; + +public class FFmpegTask { + + public void process(MediaSession session) { + final var ffmpeg = new FFmpegCommandBuilder(); + for (Parameter p : session.getParams()) { + p.accept(ffmpeg, session); + } + + try { + final ProcessBuilder pb = new ProcessBuilder(ffmpeg.buildCommand(session)); + pb.redirectErrorStream(true); + pb.inheritIO(); + session.setStatus("Starting ffmpeg"); + final Process process = pb.start(); + final Scanner out = new Scanner(process.getInputStream()); + while (out.hasNextLine()) { + final String line = out.nextLine(); + session.setStatus(line); + } + process.waitFor(); + } catch (InterruptedException | IOException e) { + session.setStatus("Failed due to " + e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/InputParametersBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/InputParametersBundle.java new file mode 100644 index 0000000..b7da5e0 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/InputParametersBundle.java @@ -0,0 +1,65 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +import com.annimon.ffmpegbot.session.MediaSession; +import com.annimon.ffmpegbot.session.Sessions; +import com.annimon.tgbotsmodule.api.methods.Methods; +import com.annimon.tgbotsmodule.commands.CommandBundle; +import com.annimon.tgbotsmodule.commands.CommandRegistry; +import com.annimon.tgbotsmodule.commands.SimpleRegexCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.MessageContext; +import com.annimon.tgbotsmodule.commands.context.RegexMessageContext; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +public class InputParametersBundle implements CommandBundle { + + private final Sessions sessions; + + public InputParametersBundle(Sessions sessions) { + this.sessions = sessions; + } + + @Override + public void register(@NotNull CommandRegistry commands) { + commands.register(new SimpleRegexCommand( + Pattern.compile("^/(ss|to?) ?(\\d+|\\d{2}:\\d{2}:\\d{2})?$"), + sessionCommand(this::cutCommand))); + } + + private void cutCommand(RegexMessageContext ctx, MediaSession session) { + final var arg = ctx.group(2); + final var inputParams = session.getInputParams(); + switch (ctx.group(1)) { + case "ss" -> inputParams.setStartPosition(arg); + case "to" -> inputParams.setEndPosition(arg); + case "t" -> inputParams.setDuration(arg); + } + editMessage(ctx, session); + } + + private void editMessage(MessageContext ctx, MediaSession session) { + Methods.editMessageText() + .setChatId(session.getChatId()) + .setMessageId(session.getMessageId()) + .setText(session.toString()) + .enableHtml() + .setReplyMarkup(ctx.message().getReplyToMessage().getReplyMarkup()) + .callAsync(ctx.sender); + } + + private Consumer sessionCommand(BiConsumer consumer) { + return ctx -> { + final var msg = ctx.message().getReplyToMessage(); + if (msg == null) return; + + final var session = sessions.get(msg.getChatId(), msg.getMessageId()); + if (session == null) return; + + consumer.accept(ctx, session); + }; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java new file mode 100644 index 0000000..c16eb37 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java @@ -0,0 +1,154 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +import com.annimon.ffmpegbot.parameters.Parameter; +import com.annimon.ffmpegbot.session.FilePath; +import com.annimon.ffmpegbot.session.MediaSession; +import com.annimon.ffmpegbot.session.Resolver; +import com.annimon.ffmpegbot.session.Sessions; +import com.annimon.tgbotsmodule.api.methods.Methods; +import com.annimon.tgbotsmodule.commands.CommandBundle; +import com.annimon.tgbotsmodule.commands.CommandRegistry; +import com.annimon.tgbotsmodule.commands.SimpleCallbackQueryCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.CallbackQueryContext; +import com.annimon.tgbotsmodule.services.CommonAbsSender; +import org.jetbrains.annotations.NotNull; +import org.telegram.telegrambots.meta.api.objects.Message; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static com.annimon.ffmpegbot.commands.ffmpeg.CallbackQueryCommands.*; +import static com.annimon.ffmpegbot.commands.ffmpeg.MediaProcessingKeyboard.createKeyboard; + +public class MediaProcessingBundle implements CommandBundle { + + private final Sessions sessions; + + public MediaProcessingBundle(Sessions sessions) { + this.sessions = sessions; + } + + @Override + public void register(@NotNull CommandRegistry commands) { + commands.register(new SimpleCallbackQueryCommand(PREV, ctx -> toggleParameter(ctx, true))); + commands.register(new SimpleCallbackQueryCommand(NEXT, ctx -> toggleParameter(ctx, false))); + commands.register(new SimpleCallbackQueryCommand(DETAIL, sessionCommand(this::details))); + commands.register(new SimpleCallbackQueryCommand(PROCESS, sessionCommand(this::process))); + } + + public void handleMessage(final @NotNull CommonAbsSender sender, final @NotNull Message message) { + final var fileInfo = Resolver.resolveFileInfo(message); + if (fileInfo == null) return; + + final var session = new MediaSession(); + session.setChatId(message.getChatId()); + session.setFileId(fileInfo.fileId()); + session.setFileType(fileInfo.fileType()); + session.setOriginalFilename(fileInfo.filename()); + session.setParams(Resolver.resolveParameters(fileInfo.fileType())); + + final var result = Methods.sendMessage() + .setChatId(message.getChatId()) + .setText(session.toString()) + .enableHtml() + .setReplyToMessageId(message.getMessageId()) + .call(sender); + + session.setMessageId(result.getMessageId()); + sessions.put(session); + + Methods.editMessageReplyMarkup(result.getChatId(), result.getMessageId()) + .setReplyMarkup(createKeyboard(session)) + .call(sender); + } + + private void toggleParameter(final CallbackQueryContext ctx, final boolean toLeft) { + final var msg = ctx.message(); + if (msg == null) return; + + final var session = sessions.get(msg.getChatId(), msg.getMessageId()); + if (session == null) return; + + final String id = ctx.argument(0); + final var parameters = session.getParams().stream() + .filter(p -> p.getId().equals(id)) + .collect(Collectors.toSet()); + if (parameters.isEmpty()) return; + + for (Parameter param : parameters) { + if (toLeft) { + param.toggleLeft(); + } else { + param.toggleRight(); + } + } + editMessage(ctx, session); + } + + private void details(final CallbackQueryContext ctx, final MediaSession session) { + final String id = ctx.argument(0); + session.getParams().stream() + .filter(p -> p.getId().equals(id)) + .findFirst() + .ifPresent(p -> + ctx.answerAsAlert(p.describe()).callAsync(ctx.sender)); + } + + private void process(final CallbackQueryContext ctx, final MediaSession session) { + if (!session.isDownloaded()) { + download(ctx, session); + } + if (!session.isDownloaded()) { + session.setStatus("The file is not downloaded yet"); + editMessage(ctx, session); + return; + } + CompletableFuture.runAsync(() -> new FFmpegTask().process(session)) + .thenRunAsync(() -> { + editMessage(ctx, session); + Resolver.resolveMethod(session.getFileType()) + .setChatId(session.getChatId()) + .setFile(session.getOutputFile()) + .call(ctx.sender); + }) + .exceptionallyAsync(throwable -> { + editMessage(ctx, session); + return null; + }); + } + + private void download(final CallbackQueryContext ctx, final MediaSession session) { + try { + final var tgFile = Methods.getFile(session.getFileId()).call(ctx.sender); + final var localFilename = FilePath.generateFilename(tgFile.getFileId(), tgFile.getFilePath()); + session.setInputFile(ctx.sender.downloadFile(tgFile, FilePath.inputFile(localFilename))); + session.setOutputFile(FilePath.outputFile(localFilename)); + } catch (TelegramApiException e) { + session.setStatus("Unable to download due to " + e.getMessage()); + editMessage(ctx, session); + } + } + + private void editMessage(CallbackQueryContext ctx, MediaSession session) { + ctx.editMessage(session.toString()) + .enableHtml() + .setReplyMarkup(createKeyboard(session)) + .callAsync(ctx.sender); + } + + private Consumer sessionCommand(BiConsumer consumer) { + return ctx -> { + final var msg = ctx.message(); + if (msg == null) return; + + final var session = sessions.get(msg.getChatId(), msg.getMessageId()); + if (session == null) return; + + consumer.accept(ctx, session); + }; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingKeyboard.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingKeyboard.java new file mode 100644 index 0000000..d35d3fb --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingKeyboard.java @@ -0,0 +1,43 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +import com.annimon.ffmpegbot.parameters.Parameter; +import com.annimon.ffmpegbot.session.MediaSession; +import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; +import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; + +import java.util.ArrayList; +import java.util.List; + +import static com.annimon.ffmpegbot.commands.ffmpeg.CallbackQueryCommands.*; + +public class MediaProcessingKeyboard { + public static InlineKeyboardMarkup createKeyboard(MediaSession mediaSession) { + final var keyboard = new ArrayList>(); + for (Parameter param : mediaSession.getParams()) { + final String paramId = param.getId(); + keyboard.add(List.of( + inlineKeyboardButton("<", callbackData(PREV, paramId)), + inlineKeyboardButton(param.describe(), callbackData(DETAIL, paramId)), + inlineKeyboardButton(">", callbackData(NEXT, paramId)) + )); + } + keyboard.add(List.of(inlineKeyboardButton("Process", callbackData(PROCESS)))); + return new InlineKeyboardMarkup(keyboard); + } + + private static InlineKeyboardButton inlineKeyboardButton(String text, String callbackData) { + final var button = new InlineKeyboardButton(); + button.setText(text); + button.setCallbackData(callbackData); + return button; + } + + @SuppressWarnings("SameParameterValue") + private static String callbackData(String command) { + return command; + } + + private static String callbackData(String command, String data) { + return command + ":" + data; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/Visitor.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/Visitor.java new file mode 100644 index 0000000..2500b92 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/Visitor.java @@ -0,0 +1,16 @@ +package com.annimon.ffmpegbot.commands.ffmpeg; + +import com.annimon.ffmpegbot.parameters.*; + +public interface Visitor { + void visit(DisableAudio p, I input); + void visit(AudioBitrate p, I input); + void visit(AudioEffect p, I input); + void visit(AudioPitch p, I input); + void visit(AudioVolume p, I input); + void visit(SpeedFactor p, I input); + void visit(VideoBitrate p, I input); + void visit(VideoScale p, I input); + void visit(VideoFrameRate p, I input); + void visit(OutputFormat p, I input); +} \ No newline at end of file diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java new file mode 100644 index 0000000..665dff0 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java @@ -0,0 +1,34 @@ +package com.annimon.ffmpegbot.commands.ytdlp; + +import com.annimon.ffmpegbot.session.FilePath; +import com.annimon.ffmpegbot.session.YtDlpSession; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class YtDlpCommandBuilder { + + public String[] buildCommand(final @NotNull YtDlpSession session) { + final var commands = new ArrayList(); + commands.add("yt-dlp"); + // Format + commands.add("-f"); + String downloadOption = session.getDownloadOption(); + if (downloadOption.equals("audio")) { + commands.add("bestaudio[ext=m4a]/bestaudio"); + } else { + final var mp4 = "bestvideo[ext=mp4][height<=%s]+bestaudio[ext=m4a]".formatted(downloadOption); + final var any = "bestvideo[height<=%s]+bestaudio".formatted(downloadOption); + final var other = "best"; + commands.add(String.join("/", List.of(mp4, any, other))); + } + // Url + commands.add(session.getUrl()); + // Output + commands.add("-o"); + commands.add(FilePath.outputDir() + "/" + session.getOutputFilename()); + System.out.println(String.join(" ", commands)); + return commands.toArray(String[]::new); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java new file mode 100644 index 0000000..c056750 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java @@ -0,0 +1,48 @@ +package com.annimon.ffmpegbot.commands.ytdlp; + +import com.annimon.ffmpegbot.session.*; +import com.annimon.tgbotsmodule.commands.CommandBundle; +import com.annimon.tgbotsmodule.commands.CommandRegistry; +import com.annimon.tgbotsmodule.commands.SimpleRegexCommand; +import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.RegexMessageContext; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +public class YtDlpCommandBundle implements CommandBundle { + @Override + public void register(@NotNull CommandRegistry commands) { + commands.register(new SimpleRegexCommand( + Pattern.compile("/dl (https?://[^ ]+) ?(audio|\\d+)?p?"), + this::download)); + } + + private void download(@NotNull RegexMessageContext ctx) { + final String url = ctx.group(1); + final String downloadOption = ctx.group(2).isEmpty() ? "720": ctx.group(2); + final var fileType = downloadOption.equals("audio") ? FileType.AUDIO : FileType.VIDEO; + final var ytDlpSession = new YtDlpSession(url, downloadOption, fileType); + final var filename = FilePath.generateFilename(url, Resolver.resolveDefaultFilename(fileType)); + ytDlpSession.setOutputFilename(filename); + + CompletableFuture.runAsync(() -> new YtDlpTask().process(ytDlpSession)) + .thenRunAsync(() -> { + final File outputFile = FilePath.outputFile(ytDlpSession.getOutputFilename()); + if (!outputFile.exists()) { + throw new RuntimeException("No file to send. Check your command settings."); + } + Resolver.resolveMethod(fileType) + .setChatId(ctx.chatId()) + .setFile(outputFile) + .call(ctx.sender); + }) + .exceptionallyAsync(throwable -> { + ctx.replyToMessage("Failed due to " + throwable.getMessage()) + .callAsync(ctx.sender); + return null; + }); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpTask.java b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpTask.java new file mode 100644 index 0000000..a9704e7 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpTask.java @@ -0,0 +1,21 @@ +package com.annimon.ffmpegbot.commands.ytdlp; + +import com.annimon.ffmpegbot.session.YtDlpSession; + +import java.io.IOException; + +public class YtDlpTask { + + public void process(YtDlpSession session) { + final var commandBuilder = new YtDlpCommandBuilder(); + try { + final ProcessBuilder pb = new ProcessBuilder(commandBuilder.buildCommand(session)); + pb.redirectErrorStream(true); + pb.inheritIO(); + final Process process = pb.start(); + process.waitFor(); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/AudioBitrate.java b/src/main/java/com/annimon/ffmpegbot/parameters/AudioBitrate.java new file mode 100644 index 0000000..234d861 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/AudioBitrate.java @@ -0,0 +1,31 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class AudioBitrate extends StringParameter { + private static final List VALUES = List.of( + "", + "4k", "16k", "32k", "64k", "128k", + "256k", "320k", "512k" + ); + + public AudioBitrate() { + super("abitrate", "Audio bitrate", VALUES, ""); + } + + @Override + public String describe() { + if (value.isEmpty()) { + return describeValue("AUTO"); + } else { + return super.describe(); + } + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/AudioEffect.java b/src/main/java/com/annimon/ffmpegbot/parameters/AudioEffect.java new file mode 100644 index 0000000..338ba1a --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/AudioEffect.java @@ -0,0 +1,35 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class AudioEffect extends StringParameter { + public static final String ROBOT = "Robot"; + public static final String ECHO = "Echo"; + public static final String ECHO_2 = "Echo 2"; + public static final String PULSATOR = "Pulsator"; + public static final String VIBRATO = "Vibrato"; + + private static final List VALUES = List.of( + "", ROBOT, ECHO, ECHO_2, PULSATOR, VIBRATO + ); + + public AudioEffect() { + super("aeffect", "Audio effect", VALUES, ""); + } + + @Override + public String describe() { + if (value.isEmpty()) { + return describeValue("NONE"); + } else { + return super.describe(); + } + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/AudioPitch.java b/src/main/java/com/annimon/ffmpegbot/parameters/AudioPitch.java new file mode 100644 index 0000000..ba32942 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/AudioPitch.java @@ -0,0 +1,22 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class AudioPitch extends StringParameter { + private static final List VALUES = List.of( + "0.6", "0.8", "0.9", + "1", + "1.15", "1.25", "1.5" + ); + + public AudioPitch() { + super("apitch", "Audio pitch", VALUES, "1"); + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/AudioVolume.java b/src/main/java/com/annimon/ffmpegbot/parameters/AudioVolume.java new file mode 100644 index 0000000..bf5b933 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/AudioVolume.java @@ -0,0 +1,29 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class AudioVolume extends StringParameter { + private static final List VALUES = List.of( + "-15dB", "-10dB", "-5dB", "-2dB", "", "2dB", "5dB", "10dB", "15dB" + ); + + public AudioVolume() { + super("volume", "Volume", VALUES, ""); + } + + @Override + public String describe() { + if (value.isEmpty()) { + return describeValue("ORIGINAL"); + } else { + return super.describe(); + } + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/BooleanParameter.java b/src/main/java/com/annimon/ffmpegbot/parameters/BooleanParameter.java new file mode 100644 index 0000000..5d3ccc9 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/BooleanParameter.java @@ -0,0 +1,23 @@ +package com.annimon.ffmpegbot.parameters; + +import java.util.List; + +public abstract class BooleanParameter extends Parameter { + public BooleanParameter(String id, String name, Boolean value) { + super(id, name, List.of(false, true), value); + } + + @Override + public String describe() { + if (value) { + return describeValue("ON"); + } else { + return describeValue("OFF"); + } + } + + @Override + protected void toggle(int dir) { + value = !value; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/DisableAudio.java b/src/main/java/com/annimon/ffmpegbot/parameters/DisableAudio.java new file mode 100644 index 0000000..c686edb --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/DisableAudio.java @@ -0,0 +1,14 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +public class DisableAudio extends BooleanParameter { + public DisableAudio() { + super("noaud", "Disable audio", false); + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java b/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java new file mode 100644 index 0000000..7c5fe63 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java @@ -0,0 +1,49 @@ +package com.annimon.ffmpegbot.parameters; + +import java.util.StringJoiner; + +public class InputParameters { + private String startPosition = ""; + private String duration = ""; + private String endPosition = ""; + + public String getStartPosition() { + return startPosition; + } + + public void setStartPosition(String ss) { + this.startPosition = ss; + } + + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + this.endPosition = ""; + } + + public String getEndPosition() { + return endPosition; + } + + public void setEndPosition(String endPosition) { + this.endPosition = endPosition; + this.duration = ""; + } + + public StringJoiner describe() { + final var joiner = new StringJoiner("\n"); + if (!startPosition.isEmpty()) { + joiner.add("Start: %s".formatted(startPosition)); + } + if (!endPosition.isEmpty()) { + joiner.add("End: %s".formatted(endPosition)); + } + if (!duration.isEmpty()) { + joiner.add("Duration: %s".formatted(duration)); + } + return joiner; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java b/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java new file mode 100644 index 0000000..16d3ca0 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/OutputFormat.java @@ -0,0 +1,28 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class OutputFormat extends StringParameter { + public static final String VIDEO = "VIDEO"; + public static final String AUDIO = "AUDIO"; + public static final String VIDEO_NOTE = "VIDEO NOTE"; + + public static OutputFormat forVideo() { + return new OutputFormat(List.of(VIDEO, AUDIO), VIDEO); + } + + public static OutputFormat forVideoNote() { + return new OutputFormat(List.of(VIDEO_NOTE, VIDEO, AUDIO), VIDEO_NOTE); + } + + public OutputFormat(List values, String initialValue) { + super("output", "Output", values, initialValue); + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/Parameter.java b/src/main/java/com/annimon/ffmpegbot/parameters/Parameter.java new file mode 100644 index 0000000..7f7519f --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/Parameter.java @@ -0,0 +1,70 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.Preconditions.checkArgument; + +public abstract class Parameter { + protected final String id; + protected final String displayName; + protected final List possibleValues; + protected T value; + + protected Parameter(String id, String displayName, List values, T value) { + this.id = id; + this.displayName = displayName; + this.possibleValues = values; + this.value = value; + checkArgument(!values.isEmpty(), "possible values cannot be empty"); + checkArgument(values.contains(value), "possible values must contain a value"); + } + + public String getId() { + return id; + } + + public T getValue() { + return value; + } + + public List getPossibleValues() { + return possibleValues; + } + + public abstract void accept(Visitor visitor, I input); + + public String describe() { + return describeValue(Objects.toString(value)); + } + + protected final String describeValue(String customValue) { + return displayName + ": " + customValue; + } + + public void toggleLeft() { + toggle(-1); + } + + public void toggleRight() { + toggle(+1); + } + + protected void toggle(int dir) { + final int size = possibleValues.size(); + if (size == 1) return; + + int nextIndex = possibleValues.indexOf(value) + dir; + if (nextIndex >= size) nextIndex -= size; + else if (nextIndex < 0) nextIndex += size; + + value = possibleValues.get(nextIndex); + } + + @Override + public String toString() { + return "[" + id + "] " + displayName + ": " + value; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/Parameters.java b/src/main/java/com/annimon/ffmpegbot/parameters/Parameters.java new file mode 100644 index 0000000..6b4b763 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/Parameters.java @@ -0,0 +1,55 @@ +package com.annimon.ffmpegbot.parameters; + +import java.util.List; + +public class Parameters { + + public static List> forAudio() { + return List.of( + new AudioBitrate(), + new AudioEffect(), + new AudioPitch(), + new AudioVolume(), + new SpeedFactor() + ); + } + + public static List> forAnimation() { + return List.of( + new VideoBitrate(), + new VideoScale(), + new VideoFrameRate(), + new SpeedFactor() + ); + } + + public static List> forVideo() { + return List.of( + new DisableAudio(), + new AudioBitrate(), + new AudioEffect(), + new AudioPitch(), + new AudioVolume(), + new VideoBitrate(), + new VideoScale(), + new VideoFrameRate(), + new SpeedFactor(), + OutputFormat.forVideo() + ); + } + + public static List> forVideoNote() { + return List.of( + new DisableAudio(), + new AudioBitrate(), + new AudioEffect(), + new AudioPitch(), + new AudioVolume(), + new VideoBitrate(), + new VideoScale(), + new VideoFrameRate(), + new SpeedFactor(), + OutputFormat.forVideoNote() + ); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/SpeedFactor.java b/src/main/java/com/annimon/ffmpegbot/parameters/SpeedFactor.java new file mode 100644 index 0000000..4f9fb5c --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/SpeedFactor.java @@ -0,0 +1,22 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class SpeedFactor extends StringParameter { + private static final List VALUES = List.of( + "0.5", "0.75", "0.8", "0.9", + "1", "1.25", "1.4", "1.5", + "1.6", "1.8", "2", "2.5", "3" + ); + + public SpeedFactor() { + super("speed", "Speed", VALUES, "1"); + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/StringParameter.java b/src/main/java/com/annimon/ffmpegbot/parameters/StringParameter.java new file mode 100644 index 0000000..a7fde23 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/StringParameter.java @@ -0,0 +1,9 @@ +package com.annimon.ffmpegbot.parameters; + +import java.util.List; + +public abstract class StringParameter extends Parameter { + public StringParameter(String id, String name, List possibleValues, String value) { + super(id, name, possibleValues, value); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/VideoBitrate.java b/src/main/java/com/annimon/ffmpegbot/parameters/VideoBitrate.java new file mode 100644 index 0000000..47fc200 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/VideoBitrate.java @@ -0,0 +1,31 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class VideoBitrate extends StringParameter { + private static final List VALUES = List.of( + "", + "16k", "32k", "64k", "128k", "256k", + "512k", "1M", "2M", "4M", "8M", "16M" + ); + + public VideoBitrate() { + super("vbitrate", "Video bitrate", VALUES, ""); + } + + @Override + public String describe() { + if (value.isEmpty()) { + return describeValue("AUTO"); + } else { + return super.describe(); + } + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/VideoFrameRate.java b/src/main/java/com/annimon/ffmpegbot/parameters/VideoFrameRate.java new file mode 100644 index 0000000..bc2aeb9 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/VideoFrameRate.java @@ -0,0 +1,29 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class VideoFrameRate extends StringParameter { + private static final List VALUES = List.of( + "5", "10", "15", "20", "25", "", "30", "45", "60" + ); + + public VideoFrameRate() { + super("vfr", "Frame rate", VALUES, ""); + } + + @Override + public String describe() { + if (value.isEmpty()) { + return describeValue("ORIGINAL"); + } else { + return super.describe(); + } + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/VideoScale.java b/src/main/java/com/annimon/ffmpegbot/parameters/VideoScale.java new file mode 100644 index 0000000..0dfff26 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/parameters/VideoScale.java @@ -0,0 +1,29 @@ +package com.annimon.ffmpegbot.parameters; + +import com.annimon.ffmpegbot.commands.ffmpeg.Visitor; + +import java.util.List; + +public class VideoScale extends StringParameter { + private static final List VALUES = List.of( + "144", "240", "360", "480", "", "720", "1080" + ); + + public VideoScale() { + super("scale", "Scale", VALUES, ""); + } + + @Override + public String describe() { + if (value.isEmpty()) { + return describeValue("ORIGINAL"); + } else { + return describeValue(value + "p"); + } + } + + @Override + public void accept(Visitor visitor, I input) { + visitor.visit(this, input); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/FileInfo.java b/src/main/java/com/annimon/ffmpegbot/session/FileInfo.java new file mode 100644 index 0000000..04c75ef --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/FileInfo.java @@ -0,0 +1,4 @@ +package com.annimon.ffmpegbot.session; + +public record FileInfo(FileType fileType, String fileId, String filename) { +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/FilePath.java b/src/main/java/com/annimon/ffmpegbot/session/FilePath.java new file mode 100644 index 0000000..d669ae6 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/FilePath.java @@ -0,0 +1,29 @@ +package com.annimon.ffmpegbot.session; + +import org.apache.commons.io.FilenameUtils; + +import java.io.File; + +public class FilePath { + + public static String inputDir() { + return "input"; + } + + public static String outputDir() { + return "output"; + } + + public static File inputFile(String filename) { + return new File(inputDir(), filename); + } + + public static File outputFile(String filename) { + return new File(outputDir(), filename); + } + + public static String generateFilename(String fileId, String filename) { + final var ext = FilenameUtils.getExtension(filename); + return "%d_%d.%s".formatted(System.currentTimeMillis(), Math.abs(fileId.hashCode()), ext); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/FileType.java b/src/main/java/com/annimon/ffmpegbot/session/FileType.java new file mode 100644 index 0000000..cfa493e --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/FileType.java @@ -0,0 +1,9 @@ +package com.annimon.ffmpegbot.session; + +public enum FileType { + ANIMATION, + AUDIO, + VIDEO, + VIDEO_NOTE, + VOICE, +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/FileTypes.java b/src/main/java/com/annimon/ffmpegbot/session/FileTypes.java new file mode 100644 index 0000000..cdd051f --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/FileTypes.java @@ -0,0 +1,17 @@ +package com.annimon.ffmpegbot.session; + +public class FileTypes { + public static boolean canContainAudio(FileType type) { + return switch (type) { + case AUDIO, VIDEO, VIDEO_NOTE, VOICE -> true; + default -> false; + }; + } + + public static boolean canContainVideo(FileType type) { + return switch (type) { + case ANIMATION, VIDEO, VIDEO_NOTE -> true; + default -> false; + }; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java b/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java new file mode 100644 index 0000000..6d7dc64 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java @@ -0,0 +1,135 @@ +package com.annimon.ffmpegbot.session; + +import com.annimon.ffmpegbot.parameters.InputParameters; +import com.annimon.ffmpegbot.parameters.Parameter; + +import java.io.File; +import java.util.List; +import java.util.StringJoiner; + +import static com.annimon.ffmpegbot.TextUtils.readableFileSize; +import static com.annimon.ffmpegbot.TextUtils.safeHtml; + +public class MediaSession { + // Session key + private long chatId; + private int messageId; + // Media info + private FileType fileType; + private String fileId; + private String originalFilename; + // Parameters + private List> params; + private final InputParameters inputParams = new InputParameters(); + // Files + private File inputFile; + private File outputFile; + // Status + private String status; + + public long getChatId() { + return chatId; + } + + public void setChatId(long chatId) { + this.chatId = chatId; + } + + public int getMessageId() { + return messageId; + } + + public void setMessageId(int messageId) { + this.messageId = messageId; + } + + public FileType getFileType() { + return fileType; + } + + public void setFileType(FileType fileType) { + this.fileType = fileType; + } + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public List> getParams() { + return params; + } + + public InputParameters getInputParams() { + return inputParams; + } + + public void setParams(List> params) { + this.params = params; + } + + public boolean isDownloaded() { + return (inputFile != null) && (inputFile.canRead()); + } + + public File getInputFile() { + return inputFile; + } + + public void setInputFile(File inputFile) { + this.inputFile = inputFile; + } + + public File getOutputFile() { + return outputFile; + } + + public void setOutputFile(File outputFile) { + this.outputFile = outputFile; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public StringJoiner describe() { + final var joiner = new StringJoiner("\n"); + joiner.add("File ID: %s".formatted(safeHtml(fileId))); + joiner.add("Type: %s".formatted(fileType)); + joiner.merge(inputParams.describe()); + if (originalFilename != null) { + joiner.add("Filename: %s".formatted(safeHtml(originalFilename))); + } + if (inputFile != null && inputFile.canRead()) { + joiner.add("Input file: %s".formatted(safeHtml(inputFile.getName()))); + joiner.add("Size: %s".formatted(readableFileSize(inputFile.length()))); + } + if (outputFile != null && outputFile.canRead()) { + joiner.add("Output size: %s".formatted(readableFileSize(outputFile.length()))); + } + if (status != null) { + joiner.add("" + safeHtml(status) + ""); + } + return joiner; + } + + @Override + public String toString() { + return describe().toString(); + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/Resolver.java b/src/main/java/com/annimon/ffmpegbot/session/Resolver.java new file mode 100644 index 0000000..791c8af --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/Resolver.java @@ -0,0 +1,60 @@ +package com.annimon.ffmpegbot.session; + +import com.annimon.ffmpegbot.parameters.Parameter; +import com.annimon.ffmpegbot.parameters.Parameters; +import com.annimon.tgbotsmodule.api.methods.Methods; +import com.annimon.tgbotsmodule.api.methods.interfaces.MediaMessageMethod; +import org.jetbrains.annotations.NotNull; +import org.telegram.telegrambots.meta.api.objects.Message; + +import java.util.List; + +public class Resolver { + public static FileInfo resolveFileInfo(@NotNull Message message) { + if (message.hasAnimation()) { + final var att = message.getAnimation(); + return new FileInfo(FileType.ANIMATION, att.getFileId(), att.getFileName()); + } else if (message.hasAudio()) { + final var att = message.getAudio(); + return new FileInfo(FileType.AUDIO, att.getFileId(), att.getFileName()); + } else if (message.hasVideo()) { + final var att = message.getVideo(); + return new FileInfo(FileType.VIDEO, att.getFileId(), att.getFileName()); + } else if (message.hasVideoNote()) { + final var att = message.getVideoNote(); + return new FileInfo(FileType.VIDEO_NOTE, att.getFileId(), null); + } else if (message.hasVoice()) { + final var att = message.getVoice(); + return new FileInfo(FileType.VOICE, att.getFileId(), null); + } else { + return null; + } + } + + public static List> resolveParameters(@NotNull FileType fileType) { + return switch (fileType) { + case ANIMATION -> Parameters.forAnimation(); + case VIDEO -> Parameters.forVideo(); + case VIDEO_NOTE -> Parameters.forVideoNote(); + case AUDIO, VOICE -> Parameters.forAudio(); + }; + } + + public static String resolveDefaultFilename(@NotNull FileType fileType) { + return "file." + switch (fileType) { + case ANIMATION, VIDEO, VIDEO_NOTE -> "mp4"; + case AUDIO -> "mp3"; + case VOICE -> "ogg"; + }; + } + + public static MediaMessageMethod, ?> resolveMethod(@NotNull FileType fileType) { + return switch (fileType) { + case ANIMATION -> Methods.sendAnimation(); + case AUDIO -> Methods.sendAudio(); + case VIDEO -> Methods.sendVideo(); + case VIDEO_NOTE -> Methods.sendVideoNote(); + case VOICE -> Methods.sendVoice(); + }; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/Sessions.java b/src/main/java/com/annimon/ffmpegbot/session/Sessions.java new file mode 100644 index 0000000..002228b --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/Sessions.java @@ -0,0 +1,28 @@ +package com.annimon.ffmpegbot.session; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class Sessions { + private final Map sessions; + + public Sessions() { + sessions = new ConcurrentHashMap<>(); + } + + public MediaSession get(long chatId, long messageId) { + return sessions.get(mapKey(chatId, messageId)); + } + + public void put(MediaSession mediaSession) { + sessions.put(mapKey(mediaSession.getChatId(), mediaSession.getMessageId()), mediaSession); + } + + public void put(long chatId, long messageId, MediaSession mediaSession) { + sessions.put(mapKey(chatId, messageId), mediaSession); + } + + private String mapKey(long chatId, long messageId) { + return chatId + "/" + messageId; + } +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java b/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java new file mode 100644 index 0000000..eef5f7f --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java @@ -0,0 +1,34 @@ +package com.annimon.ffmpegbot.session; + +public class YtDlpSession { + private final String url; + private final String downloadOption; + private final FileType fileType; + private String outputFilename; + + public YtDlpSession(String url, String downloadOption, FileType fileType) { + this.url = url; + this.downloadOption = downloadOption; + this.fileType = fileType; + } + + public String getUrl() { + return url; + } + + public String getDownloadOption() { + return downloadOption; + } + + public FileType getFileType() { + return fileType; + } + + public String getOutputFilename() { + return outputFilename; + } + + public void setOutputFilename(String outputFilename) { + this.outputFilename = outputFilename; + } +} diff --git a/src/main/resources/config.yaml b/src/main/resources/config.yaml new file mode 100644 index 0000000..ed3a337 --- /dev/null +++ b/src/main/resources/config.yaml @@ -0,0 +1,3 @@ +log-level: FINE +modules: + - com.annimon.ffmpegbot.MainBot \ No newline at end of file