1
0
mirror of https://github.com/aNNiMON/ffmpegbot synced 2024-09-19 22:54:20 +03:00

Rework /dl command to allow trim

This commit is contained in:
aNNiMON 2023-01-24 23:59:19 +02:00
parent c38dc4323b
commit bbc2b83746
13 changed files with 163 additions and 69 deletions

View File

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

View File

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

View File

@ -129,7 +129,7 @@ public class FFmpegCommandBuilder implements Visitor<MediaSession> {
public String[] buildCommand(final @NotNull MediaSession session) {
final var commands = new ArrayList<String>();
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<MediaSession> {
commands.addAll(List.of("-y", FilePath.outputDir() + "/" + session.getOutputFile().getName()));
return commands.toArray(String[]::new);
}
private List<String> buildTrim(@NotNull MediaSession session) {
final var commands = new ArrayList<String>();
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;
}
}

View File

@ -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<For> {
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<For> {
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<For> {
.callAsync(ctx.sender);
}
private Consumer<RegexMessageContext> sessionCommand(BiConsumer<RegexMessageContext, MediaSession> consumer) {
private Consumer<RegexMessageContext> sessionCommand(BiConsumer<RegexMessageContext, Session> consumer) {
return ctx -> {
final var msg = ctx.message().getReplyToMessage();
if (msg == null) return;

View File

@ -72,7 +72,7 @@ public class MediaProcessingBundle implements CommandBundle<For> {
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<For> {
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);

View File

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

View File

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

View File

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

View File

@ -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<String> asFFmpegCommands() {
final var commands = new ArrayList<String>();
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()) {

View File

@ -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<Parameter<?>> 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<Parameter<?>> 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: <code>%s</code>".formatted(fileType));

View File

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

View File

@ -4,21 +4,29 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Sessions {
private final Map<String, MediaSession> sessions;
private final Map<String, Session> 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);
}

View File

@ -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: <code>%s</code>".formatted(url));
joiner.add("Type: <code>%s</code>".formatted(fileType));
joiner.merge(inputParams.describe());
return joiner;
}
@Override
public String toString() {
return describe().toString();
}
}