mirror of
https://github.com/headporter81/DiscordDownloaderBot.git
synced 2026-08-08 15:41:12 +09:00
296 lines
12 KiB
Java
296 lines
12 KiB
Java
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;
|
||
}
|
||
////
|
||
} |