commit d56f9846d50a0cb4c7919c553cbbd06d07f4387a Author: sungjin.choi Date: Wed Dec 10 13:33:46 2025 +0900 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b77a980 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +.kotlin + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/Downloads/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..9879198 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 쿼리 파일을 포함한 무시된 디폴트 폴더 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6774fba --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..088adda --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + com.example + DiscordDownloaderBot + 1.0 + + + + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + DiscordDownloadBot + + + + + + + + + + UTF-8 + 8 + 8 + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d922067 --- /dev/null +++ b/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.example + DiscordDownloaderBot + 1.0 + + + 8 + 8 + UTF-8 + + + + + net.dv8tion + JDA + 5.0.0-beta.19 + + + ch.qos.logback + logback-classic + 1.2.13 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + DiscordDownloadBot + + + + + + + + \ No newline at end of file diff --git a/src/main/java/DiscordDownloadBot.java b/src/main/java/DiscordDownloadBot.java new file mode 100644 index 0000000..ae4e4e5 --- /dev/null +++ b/src/main/java/DiscordDownloadBot.java @@ -0,0 +1,173 @@ +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +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.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"; + + // yt-dlp.exe 경로 (프로젝트 최상위 폴더 기준) + private static final String YTDLP_PATH = System.getProperty("user.dir") + File.separator + "yt-dlp.exe"; + + // 저장할 폴더 (프로젝트 폴더 아래 'Downloads') + private static final String DOWNLOAD_DIR = System.getProperty("user.dir") + File.separator + "Downloads"; + + // 동시에 3개까지만 다운로드 처리 (서버 부하 방지) + private final ExecutorService executor = Executors.newFixedThreadPool(3); + + public static void main(String[] args) { + // 1. 필수 파일 확인 + if (!new File(YTDLP_PATH).exists()) { + System.err.println("❌ 오류: yt-dlp.exe 파일을 프로젝트 폴더(pom.xml 옆)에 넣어주세요!"); + return; + } + + File folder = new File(DOWNLOAD_DIR); + if (!folder.exists()) { + boolean success = folder.mkdirs(); // 폴더가 없으면 생성 + if (success) { + System.out.println("✅ 다운로드 폴더를 새로 만들었습니다: " + DOWNLOAD_DIR); + } else { + System.out.println("⚠️ 폴더 생성 실패! 권한을 확인하세요."); + } + } + + // 2. 봇 실행 + try { + JDABuilder.createDefault(BOT_TOKEN) + .enableIntents(GatewayIntent.MESSAGE_CONTENT) // 메시지 내용 읽기 권한 + .addEventListeners(new DiscordDownloadBot()) + .build(); + System.out.println("🤖 봇 실행 완료! 유튜브/인스타 링크를 감지합니다."); + System.out.println("📂 저장 경로: " + DOWNLOAD_DIR); + } catch (Exception e) { + System.err.println("❌ 봇 실행 실패: 토큰이 올바른지 확인하세요."); + e.printStackTrace(); + } + } + + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + // 1. 봇 자신의 메시지는 무시 + if (event.getAuthor().isBot()) return; + + // 2. 메시지 내용 가져오기 + String message = event.getMessage().getContentRaw(); + System.out.println("👂 감지된 메시지: " + message); + + // 3. URL 추출 (작성하신 extractUrl 함수 사용) + String foundUrl = extractUrl(message); + + // 4. 유효한 링크가 발견되면 다운로드 시작 + if (foundUrl != null) { + System.out.println("✅ 다운로드 대상 링크 발견: " + foundUrl); + event.getChannel().sendMessage("📥 영상을 감지했습니다! 다운로드를 시도합니다... (" + foundUrl + ")").queue(); + + // 5. [핵심] 별도 스레드에서 다운로드 실행 (봇 멈춤 방지) + executor.submit(() -> { + processDownload(foundUrl, event); + }); + } + } + + // 유튜브, 인스타, 틱톡 등의 URL만 쏙 뽑아내는 함수 + private String extractUrl(String text) { + // http 또는 https로 시작하는 주소 패턴 + String regex = "(https?://\\S+)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(text); + + if (matcher.find()) { + String foundUrl = matcher.group(1); + // 유튜브, 인스타, 틱톡 주소인지 한번 더 체크 (원하면 이 부분 삭제해서 모든 링크 허용 가능) + if (foundUrl.contains("youtube.com") || foundUrl.contains("youtu.be") || + foundUrl.contains("instagram.com") || foundUrl.contains("tiktok.com")) { + return foundUrl; + } + } + return null; + } + + private void processDownload(String url, MessageReceivedEvent event) { + // 진행 상황을 표시할 메시지 객체 + net.dv8tion.jda.api.entities.Message progressMessage = null; + + try { + // 1. 처음에 "다운로드 시작" 메시지를 보내고, 그 메시지 객체를 가져옵니다. + progressMessage = event.getChannel().sendMessage("⏳ **다운로드 준비 중...**").complete(); + + File dir = new File(DOWNLOAD_DIR); + if (!dir.exists()) dir.mkdirs(); + + String outputPath = DOWNLOAD_DIR + File.separator + "%(title)s.%(ext)s"; + + // 2. 명령어 설정 (--newline 옵션 추가가 핵심!) + String[] command = { + YTDLP_PATH, + "--newline", // 👈 [중요] 진행률을 한 줄씩 출력하게 함 + "--no-warnings", + "-f", "bestvideo+bestaudio/best", + "--merge-output-format", "mp4", + "-o", outputPath, + url + }; + + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); // 에러도 같이 읽음 + Process process = pb.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + + // 시간 체크용 변수 (너무 잦은 업데이트 방지) + long lastUpdateTime = 0; + + // 3. yt-dlp의 출력을 한 줄씩 읽음 + while ((line = reader.readLine()) != null) { + System.out.println("[yt-dlp] " + line); // 콘솔에도 출력 + + // "[download] 25.4%" 같은 패턴을 찾음 + if (line.contains("[download]") && line.contains("%")) { + long currentTime = System.currentTimeMillis(); + + // 3초(3000ms)가 지났을 때만 디스코드 메시지 수정 + if (currentTime - lastUpdateTime > 3000) { + // 정규식으로 숫자만 쏙 뽑기 (예: 25.4%) + Matcher matcher = Pattern.compile("(\\d+\\.\\d+%)").matcher(line); + if (matcher.find()) { + String percent = matcher.group(1); + progressMessage.editMessage("📥 **다운로드 중...** " + percent).queue(); + lastUpdateTime = currentTime; + } + } + } + } + + int exitCode = process.waitFor(); + + // 4. 완료 시 메시지 수정 + if (exitCode == 0) { + progressMessage.editMessage("✅ **다운로드 완료!**\n(파일은 서버 컴퓨터의 `Downloads` 폴더에 저장되었습니다.)").queue(); + } else { + progressMessage.editMessage("❌ **다운로드 실패.** 콘솔 로그를 확인하세요.").queue(); + } + + } catch (Exception e) { + e.printStackTrace(); + if (progressMessage != null) { + progressMessage.editMessage("🔥 **오류 발생:** " + e.getMessage()).queue(); + } + } + } +} \ No newline at end of file diff --git a/yt-dlp.exe b/yt-dlp.exe new file mode 100644 index 0000000..ac0e0d8 Binary files /dev/null and b/yt-dlp.exe differ