diff --git a/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java b/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java index 7bbdc36..302d01e 100644 --- a/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java +++ b/src/main/java/com/annimon/ffmpegbot/MainBotHandler.java @@ -41,7 +41,7 @@ public class MainBotHandler extends BotHandler { mediaProcessingBundle = new MediaProcessingBundle(sessions, fallbackFileDownloader); commands.registerBundle(mediaProcessingBundle); commands.registerBundle(new InputParametersBundle(sessions)); - commands.registerBundle(new YtDlpCommandBundle()); + commands.registerBundle(new YtDlpCommandBundle(sessions)); commands.registerBundle(new AdminCommandBundle()); commands.register(new HelpCommand()); } diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java index 8d75286..104ea32 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/CallbackQueryCommands.java @@ -5,6 +5,7 @@ public final class CallbackQueryCommands { public static final String NEXT = "next"; public static final String DETAIL = "detail"; public static final String PROCESS = "process"; + public static final String YTDLP_START = "ytdlp_start"; 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 index a0a72ae..6e4c8b2 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/FFmpegCommandBuilder.java @@ -129,7 +129,7 @@ public class FFmpegCommandBuilder implements Visitor { 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(session.getInputParams().asFFmpegCommands()); commands.addAll(List.of("-i", FilePath.inputDir() + "/" + session.getInputFile().getName())); if (FileTypes.canContainAudio(session.getFileType())) { commands.addAll(audioCommands); @@ -148,22 +148,4 @@ public class FFmpegCommandBuilder implements Visitor { 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/InputParametersBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/InputParametersBundle.java index c89b330..6e33be7 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/InputParametersBundle.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/InputParametersBundle.java @@ -1,6 +1,6 @@ package com.annimon.ffmpegbot.commands.ffmpeg; -import com.annimon.ffmpegbot.session.MediaSession; +import com.annimon.ffmpegbot.session.Session; import com.annimon.ffmpegbot.session.Sessions; import com.annimon.tgbotsmodule.api.methods.Methods; import com.annimon.tgbotsmodule.commands.CommandBundle; @@ -31,7 +31,7 @@ public class InputParametersBundle implements CommandBundle { sessionCommand(this::cutCommand))); } - private void cutCommand(RegexMessageContext ctx, MediaSession session) { + private void cutCommand(RegexMessageContext ctx, Session session) { final var arg = ctx.group(2); final var inputParams = session.getInputParams(); switch (ctx.group(1)) { @@ -42,7 +42,7 @@ public class InputParametersBundle implements CommandBundle { editMessage(ctx, session); } - private void editMessage(MessageContext ctx, MediaSession session) { + private void editMessage(MessageContext ctx, Session session) { Methods.editMessageText() .setChatId(session.getChatId()) .setMessageId(session.getMessageId()) @@ -52,7 +52,7 @@ public class InputParametersBundle implements CommandBundle { .callAsync(ctx.sender); } - private Consumer sessionCommand(BiConsumer consumer) { + private Consumer sessionCommand(BiConsumer consumer) { return ctx -> { final var msg = ctx.message().getReplyToMessage(); if (msg == null) return; diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java index 7cc87bf..9193ff6 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingBundle.java @@ -72,7 +72,7 @@ public class MediaProcessingBundle implements CommandBundle { final var msg = ctx.message(); if (msg == null) return; - final var session = sessions.get(msg.getChatId(), msg.getMessageId()); + final var session = sessions.getMediaSession(msg.getChatId(), msg.getMessageId()); if (session == null) return; final String id = ctx.argument(0); @@ -157,7 +157,7 @@ public class MediaProcessingBundle implements CommandBundle { final var msg = ctx.message(); if (msg == null) return; - final var session = sessions.get(msg.getChatId(), msg.getMessageId()); + final var session = sessions.getMediaSession(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 index d35d3fb..be95356 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingKeyboard.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ffmpeg/MediaProcessingKeyboard.java @@ -2,6 +2,7 @@ package com.annimon.ffmpegbot.commands.ffmpeg; import com.annimon.ffmpegbot.parameters.Parameter; import com.annimon.ffmpegbot.session.MediaSession; +import com.annimon.ffmpegbot.session.YtDlpSession; import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; @@ -11,9 +12,9 @@ import java.util.List; import static com.annimon.ffmpegbot.commands.ffmpeg.CallbackQueryCommands.*; public class MediaProcessingKeyboard { - public static InlineKeyboardMarkup createKeyboard(MediaSession mediaSession) { + public static InlineKeyboardMarkup createKeyboard(MediaSession session) { final var keyboard = new ArrayList>(); - for (Parameter param : mediaSession.getParams()) { + for (Parameter param : session.getParams()) { final String paramId = param.getId(); keyboard.add(List.of( inlineKeyboardButton("<", callbackData(PREV, paramId)), @@ -25,6 +26,12 @@ public class MediaProcessingKeyboard { return new InlineKeyboardMarkup(keyboard); } + public static InlineKeyboardMarkup createKeyboard(YtDlpSession session) { + final var keyboard = new ArrayList>(); + keyboard.add(List.of(inlineKeyboardButton("Start", callbackData(YTDLP_START)))); + return new InlineKeyboardMarkup(keyboard); + } + private static InlineKeyboardButton inlineKeyboardButton(String text, String callbackData) { final var button = new InlineKeyboardButton(); button.setText(text); diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java index 66db61e..569fda7 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBuilder.java @@ -23,6 +23,12 @@ public class YtDlpCommandBuilder { final var other = "best"; commands.add(String.join("/", List.of(mp4, any, other))); } + // Trim + if (!session.getInputParams().isEmpty()) { + commands.addAll(List.of("--external-downloader", "ffmpeg")); + final String ffmpegArgs = String.join(" ", session.getInputParams().asFFmpegCommands()); + commands.addAll(List.of("--external-downloader-args", "ffmpeg_i:" + ffmpegArgs)); + } // Url commands.add(session.getUrl()); // Output diff --git a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java index af35e3c..72154aa 100644 --- a/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java +++ b/src/main/java/com/annimon/ffmpegbot/commands/ytdlp/YtDlpCommandBundle.java @@ -1,13 +1,16 @@ package com.annimon.ffmpegbot.commands.ytdlp; import com.annimon.ffmpegbot.Permissions; +import com.annimon.ffmpegbot.commands.ffmpeg.CallbackQueryCommands; import com.annimon.ffmpegbot.file.FilePath; import com.annimon.ffmpegbot.session.*; 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.SimpleRegexCommand; import com.annimon.tgbotsmodule.commands.authority.For; +import com.annimon.tgbotsmodule.commands.context.CallbackQueryContext; import com.annimon.tgbotsmodule.commands.context.RegexMessageContext; import org.jetbrains.annotations.NotNull; @@ -17,13 +20,26 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.regex.Pattern; +import static com.annimon.ffmpegbot.commands.ffmpeg.MediaProcessingKeyboard.createKeyboard; + public class YtDlpCommandBundle implements CommandBundle { + + private final Sessions sessions; + + public YtDlpCommandBundle(Sessions sessions) { + this.sessions = sessions; + } + @Override public void register(@NotNull CommandRegistry commands) { commands.register(new SimpleRegexCommand( Pattern.compile("/dl (https?://[^ ]+) ?(audio|\\d+)?p?"), Permissions.ALLOWED_USERS, this::download)); + commands.register(new SimpleCallbackQueryCommand( + CallbackQueryCommands.YTDLP_START, + Permissions.ALLOWED_USERS, + this::ytDlpStart)); } private void download(@NotNull RegexMessageContext ctx) { @@ -31,25 +47,48 @@ public class YtDlpCommandBundle implements CommandBundle { final String downloadOption = Optional.ofNullable(ctx.group(2)) .filter(Predicate.not(String::isBlank)) .orElse("720"); - final var fileType = downloadOption.equals("audio") ? FileType.AUDIO : FileType.VIDEO; - final var ytDlpSession = new YtDlpSession(url, downloadOption, fileType); - final var filename = FilePath.generateFilename(url, FilePath.defaultFilename(fileType)); - ytDlpSession.setOutputFilename(filename); - Methods.sendChatAction(ctx.chatId(), Resolver.resolveAction(fileType)).callAsync(ctx.sender); - CompletableFuture.runAsync(() -> new YtDlpTask().process(ytDlpSession)) + final var message = ctx.message(); + final var fileType = downloadOption.equals("audio") ? FileType.AUDIO : FileType.VIDEO; + final var filename = FilePath.generateFilename(url, FilePath.defaultFilename(fileType)); + + final var session = new YtDlpSession(url, downloadOption, fileType); + session.setChatId(message.getChatId()); + session.setOutputFilename(filename); + + final var result = ctx.replyToMessage(session.toString()) + .enableHtml() + .call(ctx.sender); + session.setMessageId(result.getMessageId()); + sessions.put(session); + + Methods.editMessageReplyMarkup(result.getChatId(), result.getMessageId()) + .setReplyMarkup(createKeyboard(session)) + .call(ctx.sender); + } + + private void ytDlpStart(CallbackQueryContext ctx) { + final var msg = ctx.message(); + if (msg == null) return; + + final var session = sessions.getYtDlpSession(msg.getChatId(), msg.getMessageId()); + if (session == null) return; + + Methods.sendChatAction(msg.getChatId(), Resolver.resolveAction(session.getFileType())) + .callAsync(ctx.sender); + CompletableFuture.runAsync(() -> new YtDlpTask().process(session)) .thenRunAsync(() -> { - final File outputFile = FilePath.outputFile(ytDlpSession.getOutputFilename()); + final File outputFile = FilePath.outputFile(session.getOutputFilename()); if (!outputFile.exists()) { throw new RuntimeException("No file to send. Check your command settings."); } - Resolver.resolveMethod(fileType) - .setChatId(ctx.chatId()) + Resolver.resolveMethod(session.getFileType()) + .setChatId(session.getChatId()) .setFile(outputFile) .call(ctx.sender); }) .exceptionallyAsync(throwable -> { - ctx.replyToMessage("Failed due to " + throwable.getMessage()) + ctx.answerAsAlert("Failed due to " + throwable.getMessage()) .callAsync(ctx.sender); return null; }); diff --git a/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java b/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java index 7c5fe63..6c0acb2 100644 --- a/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java +++ b/src/main/java/com/annimon/ffmpegbot/parameters/InputParameters.java @@ -1,6 +1,9 @@ package com.annimon.ffmpegbot.parameters; +import java.util.ArrayList; +import java.util.List; import java.util.StringJoiner; +import java.util.stream.Stream; public class InputParameters { private String startPosition = ""; @@ -33,6 +36,27 @@ public class InputParameters { this.duration = ""; } + public boolean isEmpty() { + return Stream.of(startPosition, endPosition, duration).allMatch(String::isEmpty); + } + + public List asFFmpegCommands() { + final var commands = new ArrayList(); + if (!startPosition.isEmpty()) { + commands.add("-ss"); + commands.add(startPosition); + } + if (!endPosition.isEmpty()) { + commands.add("-to"); + commands.add(endPosition); + } + if (!duration.isEmpty()) { + commands.add("-t"); + commands.add(duration); + } + return commands; + } + public StringJoiner describe() { final var joiner = new StringJoiner("\n"); if (!startPosition.isEmpty()) { diff --git a/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java b/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java index f7b3be9..707a925 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java +++ b/src/main/java/com/annimon/ffmpegbot/session/MediaSession.java @@ -1,6 +1,5 @@ package com.annimon.ffmpegbot.session; -import com.annimon.ffmpegbot.parameters.InputParameters; import com.annimon.ffmpegbot.parameters.Parameter; import java.io.File; @@ -9,10 +8,7 @@ import java.util.StringJoiner; import static com.annimon.ffmpegbot.TextUtils.*; -public class MediaSession { - // Session key - private long chatId; - private int messageId; +public final class MediaSession extends Session { // Media info private FileType fileType; private String fileId; @@ -22,7 +18,6 @@ public class MediaSession { private String dimensions; // Parameters private List> params; - private final InputParameters inputParams = new InputParameters(); // Files private File inputFile; private File outputFile; @@ -38,22 +33,6 @@ public class MediaSession { this.setOriginalFilename(fileInfo.filename()); } - 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; } @@ -94,10 +73,6 @@ public class MediaSession { return params; } - public InputParameters getInputParams() { - return inputParams; - } - public void setParams(List> params) { this.params = params; } @@ -126,6 +101,7 @@ public class MediaSession { this.status = status; } + @Override public StringJoiner describe() { final var joiner = new StringJoiner("\n"); joiner.add("Type: %s".formatted(fileType)); diff --git a/src/main/java/com/annimon/ffmpegbot/session/Session.java b/src/main/java/com/annimon/ffmpegbot/session/Session.java new file mode 100644 index 0000000..2468897 --- /dev/null +++ b/src/main/java/com/annimon/ffmpegbot/session/Session.java @@ -0,0 +1,35 @@ +package com.annimon.ffmpegbot.session; + +import com.annimon.ffmpegbot.parameters.InputParameters; + +import java.util.StringJoiner; + +public abstract sealed class Session permits MediaSession, YtDlpSession { + // Session key + protected long chatId; + protected int messageId; + // Parameters + protected final InputParameters inputParams = new InputParameters(); + + 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 InputParameters getInputParams() { + return inputParams; + } + + abstract StringJoiner describe(); +} diff --git a/src/main/java/com/annimon/ffmpegbot/session/Sessions.java b/src/main/java/com/annimon/ffmpegbot/session/Sessions.java index 002228b..4d172d1 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/Sessions.java +++ b/src/main/java/com/annimon/ffmpegbot/session/Sessions.java @@ -4,21 +4,29 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class Sessions { - private final Map sessions; + private final Map sessions; public Sessions() { sessions = new ConcurrentHashMap<>(); } - public MediaSession get(long chatId, long messageId) { + public Session get(long chatId, long messageId) { return sessions.get(mapKey(chatId, messageId)); } - public void put(MediaSession mediaSession) { + public MediaSession getMediaSession(long chatId, long messageId) { + return (MediaSession) get(chatId, messageId); + } + + public YtDlpSession getYtDlpSession(long chatId, long messageId) { + return (YtDlpSession) get(chatId, messageId); + } + + public void put(Session mediaSession) { sessions.put(mapKey(mediaSession.getChatId(), mediaSession.getMessageId()), mediaSession); } - public void put(long chatId, long messageId, MediaSession mediaSession) { + public void put(long chatId, long messageId, Session mediaSession) { sessions.put(mapKey(chatId, messageId), mediaSession); } diff --git a/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java b/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java index eef5f7f..e38c132 100644 --- a/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java +++ b/src/main/java/com/annimon/ffmpegbot/session/YtDlpSession.java @@ -1,6 +1,8 @@ package com.annimon.ffmpegbot.session; -public class YtDlpSession { +import java.util.StringJoiner; + +public final class YtDlpSession extends Session { private final String url; private final String downloadOption; private final FileType fileType; @@ -31,4 +33,18 @@ public class YtDlpSession { public void setOutputFilename(String outputFilename) { this.outputFilename = outputFilename; } + + @Override + public StringJoiner describe() { + final var joiner = new StringJoiner("\n"); + joiner.add("URL: %s".formatted(url)); + joiner.add("Type: %s".formatted(fileType)); + joiner.merge(inputParams.describe()); + return joiner; + } + + @Override + public String toString() { + return describe().toString(); + } }