Files
DiscordDownloaderBot/src/main/java/DiscordDownloadBot.java
T
2026-04-17 15:05:38 +09:00

296 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, String> pendingUrls = new HashMap<>();
private static final Map<String, Boolean> 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<String> 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<String> 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;
}
////
}