import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu; import net.dv8tion.jda.api.requests.GatewayIntent; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DiscordDownloadBot extends ListenerAdapter { // πŸ”΄ 토큰 확인 private static final String BOT_TOKEN = "MTQ0Nzg1NjI1NTY4NjE0ODEzOA.GpvmOr.KRTrSGh1GrmlF_oHY1brKZbAXb3erfHBlPXHEI"; private static final String YTDLP_PATH = System.getProperty("user.dir") + File.separator + "yt-dlp.exe"; private static final String BASE_DOWNLOAD_DIR = System.getProperty("user.dir") + File.separator + "Downloads"; private final ExecutorService executor = Executors.newFixedThreadPool(3); private static final Map pendingUrls = new HashMap<>(); private static final Map isCreatingFolder = new HashMap<>(); public static void main(String[] args) { new File(BASE_DOWNLOAD_DIR).mkdirs(); try { JDABuilder.createDefault(BOT_TOKEN) .enableIntents(GatewayIntent.MESSAGE_CONTENT) .addEventListeners(new DiscordDownloadBot()) .build(); System.out.println("πŸ€– 봇 μ‹€ν–‰ μ™„λ£Œ! (더블 νƒ­ μ „λž΅)"); } catch (Exception e) { e.printStackTrace(); } } @Override public void onMessageReceived(@NotNull MessageReceivedEvent event) { if (event.getAuthor().isBot()) return; String userId = event.getAuthor().getId(); String message = event.getMessage().getContentRaw(); if (isCreatingFolder.getOrDefault(userId, false)) { createNewFolderAndDownload(event, message); return; } String foundUrl = extractUrl(message); if (foundUrl != null) { pendingUrls.put(userId, foundUrl); sendFolderSelectionMenu(event); } } private void sendFolderSelectionMenu(MessageReceivedEvent event) { File baseDir = new File(BASE_DOWNLOAD_DIR); File[] directories = baseDir.listFiles(File::isDirectory); StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("folder-select").setPlaceholder("πŸ“‚ μ €μž₯ν•  폴더λ₯Ό μ„ νƒν•˜μ„Έμš”"); if (directories != null) { for (File dir : directories) { if (menuBuilder.getOptions().size() >= 24) break; menuBuilder.addOption("πŸ“‚ " + dir.getName(), dir.getName()); } } menuBuilder.addOption("βž• μƒˆ 폴더 λ§Œλ“€κΈ°", "create_new_folder"); event.getChannel().sendMessage("μ˜μƒ 링크λ₯Ό ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€! **어디에 μ €μž₯ν• κΉŒμš”?**").addActionRow(menuBuilder.build()).queue(); } @Override public void onStringSelectInteraction(StringSelectInteractionEvent event) { if (!event.getComponentId().equals("folder-select")) return; String selectedValue = event.getValues().get(0); String userId = event.getUser().getId(); String url = pendingUrls.get(userId); if (url == null) { event.reply("❌ 링크 정보가 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.").setEphemeral(true).queue(); return; } if (selectedValue.equals("create_new_folder")) { isCreatingFolder.put(userId, true); event.reply("πŸ“ **μƒˆ 폴더 이름을 μž…λ ₯ν•΄μ£Όμ„Έμš”.**").setEphemeral(true).queue(); } else { event.reply("βœ… **" + selectedValue + "** 폴더에 μ €μž₯을 μ‹œμž‘ν•©λ‹ˆλ‹€!").queue(); String targetPath = BASE_DOWNLOAD_DIR + File.separator + selectedValue; executor.submit(() -> processDownload(url, event.getMessageChannel(), targetPath)); pendingUrls.remove(userId); } } private void createNewFolderAndDownload(MessageReceivedEvent event, String folderName) { String userId = event.getAuthor().getId(); folderName = folderName.replaceAll("[^a-zA-Z0-9κ°€-힣_\\-]", ""); if (folderName.isEmpty()) folderName = "NewFolder"; String newFolderPath = BASE_DOWNLOAD_DIR + File.separator + folderName; new File(newFolderPath).mkdirs(); String url = pendingUrls.get(userId); if (url != null) { event.getChannel().sendMessage("πŸ“₯ **" + folderName + "** 폴더에 λ‹€μš΄λ‘œλ“œ μ‹œμž‘!").queue(); executor.submit(() -> processDownload(url, event.getChannel(), newFolderPath)); } isCreatingFolder.remove(userId); pendingUrls.remove(userId); } // ⬇️ [μ΅œμ’… μ „λž΅] 1μ°¨(μ˜μƒ) μ‹œλ„ -> μ‹€νŒ¨μ‹œ -> 2μ°¨(사진) μ‹œλ„ private void processDownload(String url, net.dv8tion.jda.api.entities.channel.middleman.MessageChannel channel, String saveDir) { Message progressMessage = null; try { progressMessage = channel.sendMessage("⏳ **λ‹€μš΄λ‘œλ“œ μ‹œλ„ 쀑...**").complete(); // 1. URLμ—μ„œ 번호 뽑기 String targetIndexStr = getImgIndex(url); String targetIndex = (targetIndexStr != null) ? targetIndexStr : "1"; System.out.println("🎯 νƒ€κ²Ÿ 번호: " + targetIndex + "번"); // ========================================== // πŸ‘Š 1타: λ™μ˜μƒμ΄λΌκ³  κ°€μ •ν•˜κ³  λ‹€μš΄λ‘œλ“œ μ‹œλ„ // ========================================== boolean isVideoSuccess = tryDownloadAsVideo(url, saveDir, targetIndex); if (isVideoSuccess) { progressMessage.editMessage("βœ… **λ™μ˜μƒ λ‹€μš΄λ‘œλ“œ μ™„λ£Œ!**\nπŸ“‚ 폴더: `" + new File(saveDir).getName() + "`").queue(); return; } System.out.println("⚠️ λ™μ˜μƒ λ‹€μš΄λ‘œλ“œ μ‹€νŒ¨(사진인듯). 2μ°¨ μ‹œλ„ μ‹œμž‘..."); // ========================================== // πŸ‘Š 2타: 사진이라고 κ°€μ •ν•˜κ³  λ‹€μš΄λ‘œλ“œ μ‹œλ„ // ========================================== boolean isImageSuccess = tryDownloadAsImage(url, saveDir, targetIndex); if (isImageSuccess) { progressMessage.editMessage("βœ… **사진 λ‹€μš΄λ‘œλ“œ μ™„λ£Œ!**\nπŸ“‚ 폴더: `" + new File(saveDir).getName() + "`").queue(); } else { progressMessage.editMessage("❌ **μ΅œμ’… μ‹€νŒ¨.** (μΏ ν‚€ 만료 λ˜λŠ” κ²Œμ‹œλ¬Ό μ‚­μ œλ¨)").queue(); } } catch (Exception e) { if (progressMessage != null) progressMessage.editMessage("πŸ”₯ 였λ₯˜: " + e.getMessage()).queue(); e.printStackTrace(); } } // πŸŽ₯ 1타: λ™μ˜μƒ λͺ¨λ“œ (Index μ§€μ •) private boolean tryDownloadAsVideo(String url, String saveDir, String index) { try { List cmdList = new ArrayList<>(); cmdList.add(YTDLP_PATH); cmdList.add("--cookies"); cmdList.add("cookies.txt"); cmdList.add("--user-agent"); // λ™μ˜μƒμ€ PC UAκ°€ 더 μ•ˆμ •μ μΌ λ•Œκ°€ 있음 cmdList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); // 🎯 인덱슀 μ§€μ • cmdList.add("--playlist-items"); cmdList.add(index); // 포맷 κ°•μ œ (λ™μ˜μƒμ΄ μ•„λ‹ˆλ©΄ μ—¬κΈ°μ„œ μ—λŸ¬ λ‚˜κ³  죽음 -> catch둜 λ„˜μ–΄κ°) cmdList.add("-f"); cmdList.add("bestvideo+bestaudio/best"); cmdList.add("--merge-output-format"); cmdList.add("mp4"); cmdList.add("-o"); cmdList.add(saveDir + File.separator + "%(title)s_%(id)s_Video" + index + ".%(ext)s"); cmdList.add(url); System.out.println("πŸš€ [1타] λ™μ˜μƒ μ‹œλ„: " + index + "번"); ProcessBuilder pb = new ProcessBuilder(cmdList); pb.redirectErrorStream(true); Process process = pb.start(); // 둜그 읽기 (성곡 μ—¬λΆ€ νŒλ‹¨) BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; boolean success = false; while ((line = reader.readLine()) != null) { // System.out.println("[Video] " + line); // λ„ˆλ¬΄ μ‹œλ„λŸ¬μš°λ©΄ 주석 처리 if (line.contains("100%") || line.contains("Destination")) { success = true; } } process.waitFor(); // 파일이 μ§„μ§œ μƒκ²ΌλŠ”μ§€ 확인 if (success) return true; // yt-dlpκ°€ μ„±κ³΅ν–ˆλ‹€κ³  ꡬ라칠 μˆ˜λ„ μžˆμœΌλ‹ˆ 파일 확인 return checkFileExists(saveDir, "Video" + index); } catch (Exception e) { e.printStackTrace(); return false; } } // πŸ–ΌοΈ 2타: 사진 λͺ¨λ“œ (Index μ§€μ • + 썸넀일 κ°•μ œ μ €μž₯) private boolean tryDownloadAsImage(String url, String saveDir, String index) { try { List cmdList = new ArrayList<>(); cmdList.add(YTDLP_PATH); cmdList.add("--cookies"); cmdList.add("cookies.txt"); cmdList.add("--user-agent"); // 사진은 λͺ¨λ°”일 UAκ°€ μ§±μž„ cmdList.add("Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"); // 🎯 인덱슀 μ§€μ • cmdList.add("--playlist-items"); cmdList.add(index); // ⭐ [핡심] "λ™μ˜μƒ 말고 썸넀일(사진)을 파일둜 써라" cmdList.add("--write-thumbnail"); cmdList.add("--skip-download"); // λ™μ˜μƒ λ‹€μš΄λ‘œλ“œλŠ” μƒλž΅ (μ–΄μ°¨ν”Ό μ—†μœΌλ‹ˆκΉŒ) cmdList.add("--convert-thumbnails"); cmdList.add("jpg"); cmdList.add("--ignore-errors"); // μ—λŸ¬ λ¬΄μ‹œ (No video μ—λŸ¬ μ”ΉκΈ°) cmdList.add("-o"); // 파일λͺ…에 ImageTagλ₯Ό λΆ™μ—¬μ„œ 식별 cmdList.add(saveDir + File.separator + "%(title)s_%(id)s_Image" + index + ".%(ext)s"); cmdList.add(url); System.out.println("πŸš€ [2타] 사진 μ‹œλ„: " + index + "번"); ProcessBuilder pb = new ProcessBuilder(cmdList); pb.redirectErrorStream(true); Process process = pb.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { // System.out.println("[Image] " + line); } process.waitFor(); // 파일이 μƒκ²ΌλŠ”μ§€ 확인 (이게 제일 확싀함) return checkFileExists(saveDir, "Image" + index); } catch (Exception e) { e.printStackTrace(); return false; } } // 파일 생성 확인 ν•¨μˆ˜ private boolean checkFileExists(String saveDir, String keyword) { File dir = new File(saveDir); File[] files = dir.listFiles(); if (files == null) return false; for (File f : files) { // 방금 λ‹€μš΄λ‘œλ“œ μ‹œλ„ν•œ 파일(ν‚€μ›Œλ“œ 포함)이 μ‘΄μž¬ν•˜λ©΄ 성곡 if (f.getName().contains(keyword)) { return true; } } return false; } private String extractUrl(String text) { Matcher matcher = Pattern.compile("(https?://\\S+)").matcher(text); if (matcher.find()) { String found = matcher.group(1); if (found.contains("youtube.com") || found.contains("youtu.be") || found.contains("instagram.com")) { return found; } } return null; } private String getImgIndex(String url) { try { Matcher m = Pattern.compile("[?&]img_index=(\\d+)").matcher(url); if (m.find()) return m.group(1); } catch (Exception e) { /* λ¬΄μ‹œ */ } return null; } //// }