diff --git a/README.md b/README.md index 3aa2210..2f64978 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # webdl -

About • @@ -15,30 +14,59 @@ Telegram bot in Java for downloading social media videos using yt-dlp ## Features -- Non-blocking message queue processing +- Non-blocking message queue processing with virtual threads -- Dynamic interaction with messages +- Dynamic interaction with Telegram messages (send, delete, edit) -- Panel with usage statistics +- Web panel with usage statistics on port 8080 + +- Automatic retry on download failures ## Quick Start & Information Webdl accepts a video URL, downloads it using [yt-dlp](https://github.com/yt-dlp/yt-dlp), and sends it back to the user as a video message. -Clone and run the project from source code. +### From source + ``` git clone https://github.com/SegoCode/webdl -cd webdl\code -mvn clean package +cd webdl/code +mvn clean package -DskipTests java -jar target/webdl.jar ``` -For Docker deployment, make sure to set up environment variables. -Use a temp volume for the download, it will delete after send. +### Docker + ``` -mvn clean package +cd webdl/code +mvn clean package -DskipTests docker build -t webdl-image . -docker run -e BOT_TOKEN=your-bot-token -p 8080:8080 -v /mnt/drive/data/webdl:/downloads --name webdl webdl-image +docker run -d \ + --name webdl \ + --restart unless-stopped \ + -e BOT_TOKEN=your-bot-token \ + -p 8080:8080 \ + -v /mnt/drive/data/webdl:/downloads \ + webdl-image +``` + +### Project structure + +``` +code/src/main/java/org/segocode/webdl/ +├── Main.java # Entry point +├── bot/ +│ ├── Webdlbot.java # Telegram long-polling bot +│ ├── constants/Messages.java # User-facing message strings +│ ├── model/{User,DataRootContainer}.java # EclipseStore persistence +│ ├── service/{MessageService,VideoService}.java +│ └── util/MessageUtil.java +├── panel/ +│ ├── PanelApplication.java # Javalin web server bootstrap +│ └── AdminController.java # Admin panel route handler +└── system/ + ├── command/CommandExecutor.java # yt-dlp subprocess with retry + └── util/FileUtil.java ``` diff --git a/code/Dockerfile b/code/Dockerfile index 7121837..63189a7 100644 --- a/code/Dockerfile +++ b/code/Dockerfile @@ -8,17 +8,11 @@ COPY src ./src RUN mvn package -DskipTests # Runtime stage: Run the application -FROM alpine:latest +FROM eclipse-temurin:21-jre-alpine -RUN apk update && \ - apk add --no-cache wget curl ffmpeg && \ +RUN apk add --no-cache ffmpeg python3 && \ wget -O /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp && \ - chmod a+rx /usr/local/bin/yt-dlp && \ - rm -rf /var/cache/apk/* - -RUN apk add --no-cache openjdk21 python3 py3-pip - -ENV YT_DLP=/usr/bin/yt-dlp PYTHONUNBUFFERED=1 + chmod a+rx /usr/local/bin/yt-dlp WORKDIR /app COPY --from=build /app/target/webdl.jar /app/webdl.jar diff --git a/code/pom.xml b/code/pom.xml index bf1f834..b4c1b7d 100644 --- a/code/pom.xml +++ b/code/pom.xml @@ -65,13 +65,25 @@ - org.segocode.Main + org.segocode.webdl.Main + + com.diffplug.spotless + spotless-maven-plugin + 3.6.0 + + + + 2.92.0 + + + + diff --git a/code/src/main/java/org/segocode/bot/service/MessageService.java b/code/src/main/java/org/segocode/bot/service/MessageService.java deleted file mode 100644 index 9a2c996..0000000 --- a/code/src/main/java/org/segocode/bot/service/MessageService.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.segocode.bot.service; - -import org.telegram.telegrambots.meta.api.methods.send.SendMessage; -import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; - -public class MessageService { - - /** - * Sends a text message to a specific chat in Telegram. - * - * @param chatId The ID of the chat where the message will be sent. - * @param replyToMessageId The ID of the message to which this message will be a reply. - * @param text The text content of the message to be sent. - * @return The SendMessage object configured with the chat ID, text, and reply to message ID. - */ - public static SendMessage sendTextMessage(Long chatId, Integer replyToMessageId, String text) { - SendMessage message = new SendMessage(chatId.toString(), text); - message.setReplyToMessageId(replyToMessageId); - return message; - } - - /** - * Deletes a specific message in a Telegram chat. - * - * @param chatId The ID of the chat where the message is located. - * @param messageId The ID of the message to be deleted. - * @return A DeleteMessage object configured with the chat ID and message ID. - */ - public static DeleteMessage deleteMessage(Long chatId, Integer messageId) { - return new DeleteMessage(chatId.toString(), messageId); - } -} diff --git a/code/src/main/java/org/segocode/bot/service/VideoService.java b/code/src/main/java/org/segocode/bot/service/VideoService.java deleted file mode 100644 index 79d853b..0000000 --- a/code/src/main/java/org/segocode/bot/service/VideoService.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.segocode.bot.service; - -import org.segocode.system.util.FileUtil; -import org.slf4j.LoggerFactory; -import org.telegram.telegrambots.meta.api.methods.send.SendVideo; -import org.telegram.telegrambots.meta.api.objects.InputFile; - -import java.io.File; -import org.slf4j.Logger; - - -public class VideoService { - private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); - - /** - * Sends a video to a specific chat in Telegram. - * - * @param chatId The ID of the chat where the video will be sent. - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The SendVideo object configured with the chat ID and reply to message ID. - */ - public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { - String filePath = buildFilePath(replyToMessageId); - LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); - - File videoFile = FileUtil.locateVideoFile(filePath); - LOGGER.info("Video file located successfully: {}", videoFile.getPath()); - - SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); - LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); - - return sendVideoRequest; - } - - /** - * Creates a SendVideo request for sending a video. - * - * @param chatId The ID of the chat where the video will be sent. - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @param videoFile The video file to be sent. - * @return A SendVideo object configured with the chat ID, reply ID, and video file. - */ - private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMessageId, File videoFile) { - SendVideo sendVideoRequest = new SendVideo(); - sendVideoRequest.setChatId(chatId.toString()); - sendVideoRequest.setReplyToMessageId(replyToMessageId); - sendVideoRequest.setVideo(new InputFile(videoFile)); - return sendVideoRequest; - } - - /** - * Builds the file path for the video file based on the given replyToMessageId. - * - * @param replyToMessageId The ID of the message to which this video will be a reply. - * @return The file path as a String. - */ - private static String buildFilePath(Integer replyToMessageId) { - return "./downloads/" + replyToMessageId + ".mp4"; - } - - -} diff --git a/code/src/main/java/org/segocode/panel/PanelController.java b/code/src/main/java/org/segocode/panel/PanelController.java deleted file mode 100644 index f8d6fc7..0000000 --- a/code/src/main/java/org/segocode/panel/PanelController.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.segocode.panel; - -import com.google.gson.Gson; -import io.javalin.Javalin; -import io.javalin.http.Context; -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.segocode.bot.model.DataRootContainer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class PanelController { - private static final Logger LOGGER = LoggerFactory.getLogger(PanelController.class); - private final Javalin app; - private final EmbeddedStorageManager storageManager; - - // Singleton instance - private static PanelController instance; - - // Private constructor to prevent instantiation - private PanelController(EmbeddedStorageManager storageManager) { - this.storageManager = storageManager; - this.app = Javalin.create().start(8080); - - // Configure routes - configureRoutes(); - - LOGGER.info("Panel controller started on port 8080"); - } - - public static synchronized void start(EmbeddedStorageManager storageManager) { - if (instance == null) { - instance = new PanelController(storageManager); - } - } - - private void configureRoutes() { - app.get("/", this::handleAdminRequest); - } - -private void handleAdminRequest(Context ctx) { - ctx.contentType("text/html"); - - try { - InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); - String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - String jsonData = new Gson().toJson(((DataRootContainer)storageManager.root()).getUsers()); - htmlContent = htmlContent.replace("{{!user_data}}", jsonData); - - ctx.result(htmlContent); - } catch (IOException e) { - LOGGER.error("Failed to read admin HTML file", e); - ctx.status(500); - ctx.result("Error loading admin panel: " + e.getMessage()); - } -} -} \ No newline at end of file diff --git a/code/src/main/java/org/segocode/Main.java b/code/src/main/java/org/segocode/webdl/Main.java similarity index 81% rename from code/src/main/java/org/segocode/Main.java rename to code/src/main/java/org/segocode/webdl/Main.java index 0b4b414..a20f823 100644 --- a/code/src/main/java/org/segocode/Main.java +++ b/code/src/main/java/org/segocode/webdl/Main.java @@ -1,18 +1,14 @@ -package org.segocode; +package org.segocode.webdl; -import io.javalin.Javalin; import org.eclipse.store.storage.embedded.types.EmbeddedStorage; import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.eclipse.store.storage.types.StorageManager; -import org.segocode.bot.Webdlbot; -import org.segocode.panel.PanelController; +import org.segocode.webdl.bot.Webdlbot; +import org.segocode.webdl.panel.PanelApplication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; -import java.util.Date; - public class Main { private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); @@ -25,7 +21,7 @@ public static void main(String[] args) { botsApi.registerBot(new Webdlbot(storageManager)); LOGGER.info("Bot started successfully and ready to download videos 🚀"); LOGGER.info("Starting webp anel app..."); - PanelController.start(storageManager); + PanelApplication.start(storageManager); } catch (Exception e) { LOGGER.error("Error while attempting to start the bot. Error details:", e); } diff --git a/code/src/main/java/org/segocode/bot/Webdlbot.java b/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java similarity index 56% rename from code/src/main/java/org/segocode/bot/Webdlbot.java rename to code/src/main/java/org/segocode/webdl/bot/Webdlbot.java index 11e65f1..7f77268 100644 --- a/code/src/main/java/org/segocode/bot/Webdlbot.java +++ b/code/src/main/java/org/segocode/webdl/bot/Webdlbot.java @@ -1,26 +1,25 @@ -package org.segocode.bot; +package org.segocode.webdl.bot; +import static org.segocode.webdl.bot.constants.Messages.*; +import static org.segocode.webdl.bot.util.MessageUtil.*; +import static org.segocode.webdl.system.util.FileUtil.*; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.*; import org.eclipse.store.storage.types.StorageManager; -import org.segocode.bot.model.DataRootContainer; -import org.segocode.bot.model.User; -import org.segocode.system.command.CommandExecutor; -import org.segocode.bot.service.VideoService; +import org.segocode.webdl.bot.model.DataRootContainer; +import org.segocode.webdl.bot.model.User; +import org.segocode.webdl.bot.service.MessageService; +import org.segocode.webdl.bot.service.VideoService; +import org.segocode.webdl.system.command.CommandExecutor; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.Update; -import org.segocode.bot.service.MessageService; -import org.slf4j.Logger; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; -import java.time.LocalDateTime; -import java.util.Optional; -import java.util.concurrent.*; - -import static org.segocode.bot.constants.Messages.*; -import static org.segocode.bot.util.MessageUtil.*; -import static org.segocode.system.util.FileUtil.*; - public class Webdlbot extends TelegramLongPollingBot { private static final Logger LOGGER = LoggerFactory.getLogger(Webdlbot.class); private static final String BOT_TOKEN = System.getenv("BOT_TOKEN"); @@ -35,7 +34,7 @@ public class Webdlbot extends TelegramLongPollingBot { TimeUnit.MILLISECONDS, // the time unit for the keepAliveTime argument new LinkedBlockingQueue<>(), // the queue to use for holding tasks before they are executed Thread.ofVirtual().factory() // the factory to use when creating new threads - ); + ); public Webdlbot(StorageManager storageManager) { this.storageManager = storageManager; @@ -48,7 +47,9 @@ private void initializeRootContainer() { storageManager.setRoot(new DataRootContainer()); LOGGER.info("Initialized new DataRootContainer as db root object"); } - LOGGER.info("Root db object contains {} users", ((DataRootContainer) storageManager.root()).getUsers().size()); + LOGGER.info( + "Root db object contains {} users", + ((DataRootContainer) storageManager.root()).getUsers().size()); } @Override @@ -65,16 +66,22 @@ public String getBotToken() { return BOT_TOKEN; } - @Override public void onUpdateReceived(Update update) { if (update.hasMessage() && update.getMessage().hasText()) { try { + long queuedTasks = executorService.getTaskCount() - executorService.getCompletedTaskCount(); Integer queuedMessageId; - if (executorService.getActiveCount() > 0 || !executorService.getQueue().isEmpty()) { - String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + executorService.getQueue().size() + "m)"; - LOGGER.info("Executor is busy, queuing the message from @{}", update.getMessage().getFrom().getUserName()); - queuedMessageId = execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), messageTime)).getMessageId(); + if (queuedTasks > 0) { + String messageTime = DOWNLOAD_REQUEST_QUEUED + " (<" + queuedTasks + "m)"; + LOGGER.info( + "Executor is busy, queuing the message from @{}", + update.getMessage().getFrom().getUserName()); + queuedMessageId = execute(MessageService.sendTextMessage( + update.getMessage().getChatId(), + update.getMessage().getMessageId(), + messageTime)) + .getMessageId(); } else { queuedMessageId = null; } @@ -82,48 +89,59 @@ public void onUpdateReceived(Update update) { executorService.submit(() -> { try { if (queuedMessageId != null) { - execute(MessageService.deleteMessage(update.getMessage().getChatId(), queuedMessageId)); + execute(MessageService.deleteMessage( + update.getMessage().getChatId(), queuedMessageId)); } - //Entry point download flow + // Entry point download flow dispatch(update); } catch (Exception e) { LOGGER.error("Failed to launch dispatch, error: {}", e.getMessage(), e); handleDispatchError(update, e); + } finally { + synchronized (storageManager) { + storageManager.setRoot(loadMetricsData(update)); + storageManager.storeRoot(); + } } }); } catch (Exception e) { LOGGER.error("Failed on onUpdateReceived, error: {}", e.getMessage(), e); handleDispatchError(update, e); - } finally { - storageManager.setRoot(loadMetricsData(update)); - storageManager.storeRoot(); } } } private void dispatch(Update update) throws Exception { - String url = ""; - Message message = update.getMessage(); - LOGGER.info("Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); - - if(!update.getMessage().toString().contains("http")) { - execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)).getMessageId(); - return; - } else { - url = extractUrlFromMessage(update.getMessage().getText()); - LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); - } + String url = ""; + Message message = update.getMessage(); + LOGGER.info( + "Starting message processing from @{}: {}", message.getFrom().getUserName(), message.getText()); + + if (!update.getMessage().getText().contains("http")) { + execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), NOT_VALID_LINK)) + .getMessageId(); + return; + } else { + url = extractUrlFromMessage(update.getMessage().getText()); + LOGGER.info("Extracted URL: {} from {}", url, message.getFrom().getUserName()); + } - final Integer responseId = execute(MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)).getMessageId(); - CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); - execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); - execute(MessageService.deleteMessage(message.getChatId(), responseId)); - cleanDownloadsFolder(); + final Integer responseId = execute( + MessageService.sendTextMessage(message.getChatId(), message.getMessageId(), DOWNLOAD_REQUEST)) + .getMessageId(); + CommandExecutor.executeCommand(url, String.valueOf(message.getMessageId())); + execute(VideoService.sendVideo(message.getChatId(), message.getMessageId())); + execute(MessageService.deleteMessage(message.getChatId(), responseId)); + cleanDownloadsFolder(); } private void handleDispatchError(Update update, Exception e) { try { - execute(MessageService.sendTextMessage(update.getMessage().getChatId(), update.getMessage().getMessageId(), DOWNLOAD_REQUEST_ERROR)).getMessageId(); + execute(MessageService.sendTextMessage( + update.getMessage().getChatId(), + update.getMessage().getMessageId(), + DOWNLOAD_REQUEST_ERROR)) + .getMessageId(); } catch (TelegramApiException ex) { LOGGER.error("Failed to send error message, error: {}", ex.getMessage(), ex); } finally { @@ -133,7 +151,8 @@ private void handleDispatchError(Update update, Exception e) { private DataRootContainer loadMetricsData(Update update) { // Create or update user data from the Telegram update - org.telegram.telegrambots.meta.api.objects.User telegramUser = update.getMessage().getFrom(); + org.telegram.telegrambots.meta.api.objects.User telegramUser = + update.getMessage().getFrom(); DataRootContainer rootContainer = (DataRootContainer) storageManager.root(); @@ -145,14 +164,19 @@ private DataRootContainer loadMetricsData(Update update) { User user = existingUser.get(); user.recordNewMessage(); } else { - User newUser = new User(userId, update.getMessage().getChatId(), telegramUser.getUserName(), telegramUser.getFirstName(), - telegramUser.getLastName(), telegramUser.getLanguageCode(), telegramUser.getIsPremium(), - 1,LocalDateTime.now().toString()); + User newUser = new User( + userId, + update.getMessage().getChatId(), + telegramUser.getUserName(), + telegramUser.getFirstName(), + telegramUser.getLastName(), + telegramUser.getLanguageCode(), + telegramUser.getIsPremium(), + 1, + LocalDateTime.now().toString()); rootContainer.getUsers().add(newUser); LOGGER.info("New user registered: {}", newUser.getUserName()); } return rootContainer; } } - - diff --git a/code/src/main/java/org/segocode/bot/constants/Messages.java b/code/src/main/java/org/segocode/webdl/bot/constants/Messages.java similarity index 61% rename from code/src/main/java/org/segocode/bot/constants/Messages.java rename to code/src/main/java/org/segocode/webdl/bot/constants/Messages.java index 8ab2826..868c653 100644 --- a/code/src/main/java/org/segocode/bot/constants/Messages.java +++ b/code/src/main/java/org/segocode/webdl/bot/constants/Messages.java @@ -1,8 +1,9 @@ -package org.segocode.bot.constants; +package org.segocode.webdl.bot.constants; public class Messages { public static final String NOT_VALID_LINK = "❌ That doesn't seem to be a valid link"; public static final String DOWNLOAD_REQUEST = "🔄 Download request processing..."; public static final String DOWNLOAD_REQUEST_QUEUED = "⌛ Your request is in queue..."; - public static final String DOWNLOAD_REQUEST_ERROR = "❌ Unable to download: web not supported, private post or reach limit"; + public static final String DOWNLOAD_REQUEST_ERROR = + "❌ Unable to download: web not supported, private post or reach limit"; } diff --git a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java b/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java similarity index 77% rename from code/src/main/java/org/segocode/bot/model/DataRootContainer.java rename to code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java index 98ca8a0..23c0951 100644 --- a/code/src/main/java/org/segocode/bot/model/DataRootContainer.java +++ b/code/src/main/java/org/segocode/webdl/bot/model/DataRootContainer.java @@ -1,12 +1,11 @@ -package org.segocode.bot.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +package org.segocode.webdl.bot.model; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @NoArgsConstructor @@ -21,8 +20,6 @@ public class DataRootContainer { * @return An Optional containing the user if found, empty otherwise */ public Optional findUserById(String id) { - return users.stream() - .filter(user -> id.equals(user.getId())) - .findFirst(); + return users.stream().filter(user -> id.equals(user.getId())).findFirst(); } } diff --git a/code/src/main/java/org/segocode/bot/model/User.java b/code/src/main/java/org/segocode/webdl/bot/model/User.java similarity index 95% rename from code/src/main/java/org/segocode/bot/model/User.java rename to code/src/main/java/org/segocode/webdl/bot/model/User.java index ac76581..481a310 100644 --- a/code/src/main/java/org/segocode/bot/model/User.java +++ b/code/src/main/java/org/segocode/webdl/bot/model/User.java @@ -1,11 +1,10 @@ -package org.segocode.bot.model; +package org.segocode.webdl.bot.model; +import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Data @NoArgsConstructor @AllArgsConstructor @@ -37,4 +36,4 @@ public void recordNewMessage() { this.messageCount++; this.lastMessageTime = LocalDateTime.now().toString(); } -} \ No newline at end of file +} diff --git a/code/src/main/java/org/segocode/service/MessageService.java b/code/src/main/java/org/segocode/webdl/bot/service/MessageService.java similarity index 97% rename from code/src/main/java/org/segocode/service/MessageService.java rename to code/src/main/java/org/segocode/webdl/bot/service/MessageService.java index fc53fbd..e91e25b 100644 --- a/code/src/main/java/org/segocode/service/MessageService.java +++ b/code/src/main/java/org/segocode/webdl/bot/service/MessageService.java @@ -1,4 +1,4 @@ -package org.segocode.service; +package org.segocode.webdl.bot.service; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; diff --git a/code/src/main/java/org/segocode/service/VideoService.java b/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java similarity index 71% rename from code/src/main/java/org/segocode/service/VideoService.java rename to code/src/main/java/org/segocode/webdl/bot/service/VideoService.java index 88c1b94..3019d9e 100644 --- a/code/src/main/java/org/segocode/service/VideoService.java +++ b/code/src/main/java/org/segocode/webdl/bot/service/VideoService.java @@ -1,14 +1,12 @@ -package org.segocode.service; +package org.segocode.webdl.bot.service; -import org.segocode.system.util.FileUtil; +import java.io.File; +import org.segocode.webdl.system.util.FileUtil; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.api.methods.send.SendVideo; import org.telegram.telegrambots.meta.api.objects.InputFile; -import java.io.File; -import org.slf4j.Logger; - - public class VideoService { private static final Logger LOGGER = LoggerFactory.getLogger(VideoService.class); @@ -20,16 +18,19 @@ public class VideoService { * @return The SendVideo object configured with the chat ID and reply to message ID. */ public static SendVideo sendVideo(Long chatId, Integer replyToMessageId) { - String filePath = buildFilePath(replyToMessageId); - LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); + String filePath = buildFilePath(replyToMessageId); + LOGGER.info("Locating video file for message ID {}: {}", replyToMessageId, filePath); - File videoFile = FileUtil.locateVideoFile(filePath); - LOGGER.info("Video file located successfully: {}", videoFile.getPath()); + File videoFile = FileUtil.locateVideoFile(filePath); + if (videoFile == null) { + throw new RuntimeException("Video file not found for message ID: " + replyToMessageId); + } + LOGGER.info("Video file located successfully: {}", videoFile.getPath()); - SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); - LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); + SendVideo sendVideoRequest = createSendVideoRequest(chatId, replyToMessageId, videoFile); + LOGGER.info("Video is sending. Chat ID: {}, Reply to Message ID: {}", chatId, replyToMessageId); - return sendVideoRequest; + return sendVideoRequest; } /** @@ -57,6 +58,4 @@ private static SendVideo createSendVideoRequest(Long chatId, Integer replyToMess private static String buildFilePath(Integer replyToMessageId) { return "./downloads/" + replyToMessageId + ".mp4"; } - - } diff --git a/code/src/main/java/org/segocode/bot/util/MessageUtil.java b/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java similarity index 90% rename from code/src/main/java/org/segocode/bot/util/MessageUtil.java rename to code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java index 66341ae..576f565 100644 --- a/code/src/main/java/org/segocode/bot/util/MessageUtil.java +++ b/code/src/main/java/org/segocode/webdl/bot/util/MessageUtil.java @@ -1,4 +1,4 @@ -package org.segocode.bot.util; +package org.segocode.webdl.bot.util; public class MessageUtil { public static String extractUrlFromMessage(String messageText) { diff --git a/code/src/main/java/org/segocode/webdl/panel/AdminController.java b/code/src/main/java/org/segocode/webdl/panel/AdminController.java new file mode 100644 index 0000000..c3e86f8 --- /dev/null +++ b/code/src/main/java/org/segocode/webdl/panel/AdminController.java @@ -0,0 +1,40 @@ +package org.segocode.webdl.panel; + +import com.google.gson.Gson; +import io.javalin.http.Context; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.segocode.webdl.bot.model.DataRootContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AdminController { + private static final Logger LOGGER = LoggerFactory.getLogger(AdminController.class); + private final EmbeddedStorageManager storageManager; + + public AdminController(EmbeddedStorageManager storageManager) { + this.storageManager = storageManager; + } + + public void handleAdminRequest(Context ctx) { + ctx.contentType("text/html"); + + try { + InputStream inputStream = getClass().getResourceAsStream("/views/admin.html"); + String htmlContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + String jsonData; + synchronized (storageManager) { + jsonData = new Gson().toJson(((DataRootContainer) storageManager.root()).getUsers()); + } + htmlContent = htmlContent.replace("{{!user_data}}", jsonData); + + ctx.result(htmlContent); + } catch (IOException e) { + LOGGER.error("Failed to read admin HTML file", e); + ctx.status(500); + ctx.result("Error loading admin panel: " + e.getMessage()); + } + } +} diff --git a/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java b/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java new file mode 100644 index 0000000..ab354cd --- /dev/null +++ b/code/src/main/java/org/segocode/webdl/panel/PanelApplication.java @@ -0,0 +1,28 @@ +package org.segocode.webdl.panel; + +import io.javalin.Javalin; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PanelApplication { + private static final Logger LOGGER = LoggerFactory.getLogger(PanelApplication.class); + private static PanelApplication instance; + + private PanelApplication(EmbeddedStorageManager storageManager) { + AdminController adminController = new AdminController(storageManager); + Javalin app = Javalin.create().start(8080); + configureRoutes(app, adminController); + LOGGER.info("Panel application started on port 8080"); + } + + public static synchronized void start(EmbeddedStorageManager storageManager) { + if (instance == null) { + instance = new PanelApplication(storageManager); + } + } + + private void configureRoutes(Javalin app, AdminController adminController) { + app.get("/", adminController::handleAdminRequest); + } +} diff --git a/code/src/main/java/org/segocode/system/command/CommandExecutor.java b/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java similarity index 83% rename from code/src/main/java/org/segocode/system/command/CommandExecutor.java rename to code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java index 164db28..be467a6 100644 --- a/code/src/main/java/org/segocode/system/command/CommandExecutor.java +++ b/code/src/main/java/org/segocode/webdl/system/command/CommandExecutor.java @@ -1,4 +1,4 @@ -package org.segocode.system.command; +package org.segocode.webdl.system.command; import java.io.File; import java.io.IOException; @@ -26,14 +26,16 @@ public static void executeCommand(String url, String uuid) throws Exception { // The -q option is important to prevent deadlocks by ensuring the output buffer is not filled. // TODO: make a StreamGobbler to handle buffered output and avoid deadlocks. - String[] command = { ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url }; + String[] command = {ytDlpCommand, "-q", "-S", "ext,res:720", "-o", outputPath, url}; int attempt = 0; while (attempt++ < MAX_RETRIES) { - LOGGER.info("Attempt {} of " + MAX_RETRIES, attempt); + LOGGER.info("Attempt {} of {}", attempt, MAX_RETRIES); try { Process process = new ProcessBuilder(command).start(); - if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) ? process.exitValue() == 0 : process.destroyForcibly() == null) { + if (process.waitFor(TIMEOUT, TimeUnit.SECONDS) + ? process.exitValue() == 0 + : process.destroyForcibly() == null) { LOGGER.info("Download successful for URL: {}", url); return; } else { @@ -41,7 +43,7 @@ public static void executeCommand(String url, String uuid) throws Exception { process.destroyForcibly(); } } catch (IOException e) { - LOGGER.error("I/O error occurred on attempt {} of " + MAX_RETRIES, attempt, e); + LOGGER.error("I/O error occurred on attempt {} of {}", attempt, MAX_RETRIES, e); if (attempt >= MAX_RETRIES) { throw e; } diff --git a/code/src/main/java/org/segocode/system/util/FileUtil.java b/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java similarity index 73% rename from code/src/main/java/org/segocode/system/util/FileUtil.java rename to code/src/main/java/org/segocode/webdl/system/util/FileUtil.java index 4038d69..dfcb8ee 100644 --- a/code/src/main/java/org/segocode/system/util/FileUtil.java +++ b/code/src/main/java/org/segocode/webdl/system/util/FileUtil.java @@ -1,4 +1,4 @@ -package org.segocode.system.util; +package org.segocode.webdl.system.util; import java.io.File; import java.nio.file.Files; @@ -9,28 +9,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - public class FileUtil { private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); - /** - * Deletes a specific file based on the provided ID. - * - * @param id the identifier of the file to delete - */ - // Currently, since the queue is for a single user at a time,we do not need to delete by ID. - public static void deleteFileById(int id) { - - Path path = Paths.get("./downloads/" + id + ".mp4"); - try { - boolean fileDeleted = Files.deleteIfExists(path); - LOGGER.info(fileDeleted ? "File successfully deleted: " + path : "File not found: " + path); - } catch (Exception e) { - LOGGER.error("Error deleting the file: {}", path, e); - cleanDownloadsFolder(); - } - } - /** * Deletes all files and folders within the 'downloads' folder. */ @@ -71,4 +52,4 @@ public static File locateVideoFile(String filePath) { } return null; } -} \ No newline at end of file +} diff --git a/code/src/main/resources/views/admin.html b/code/src/main/resources/views/admin.html index 77486dd..d5a472c 100644 --- a/code/src/main/resources/views/admin.html +++ b/code/src/main/resources/views/admin.html @@ -3,168 +3,455 @@ - Usage dashboard - + Dashboard + + + + - - - -

-
-
-
- - - -
-

Usage Dashboard

+ +
+
+
+

DASHBOARD

+

Bot activity & user statistics

-
- - - - Last updated: +
+
PANEL
+
UPDATED --
+
-
-
- - -
- -
- -
-
-
- - - -
-
-

Total users

-

0

-
-
+ + +
+
+

Total messages processed

+

0

- - -
-
-
- - - -
-
-

Total messages

-

0

-
+
+
+

Total users

+

0

-
- - -
-
-
- - - -
-
-

Avg messages/user

-

0

-
+
+

Average / user

+

0

-
- - -
-
-
- - - -
-
-

Active Countries

-

0

-
+
+

Active languages

+

0

-
- - -
-

User activity

-
- - - - - - - - - - - -
NameUsernameCountryMessagesLast active
+
+ +
+
+

User activity table

+

Ordered by message volume

-
+ + + + + + + + + + + +
NameUsernameLanguageMessagesLast active
+
-