mirror of
https://github.com/headporter81/U-DOWNLOADER.git
synced 2026-08-08 15:41:12 +09:00
초기 커밋
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
public class FormatItem {
|
||||
private String displayString; // 콤보박스에 표시될 문자열 (예: "1080p (mp4, 76.69MiB)")
|
||||
private String formatId; // 실제 yt-dlp 다운로드에 사용될 ID (예: "137")
|
||||
|
||||
public FormatItem(String displayString, String formatId) {
|
||||
this.displayString = displayString;
|
||||
this.formatId = formatId;
|
||||
}
|
||||
|
||||
public String getFormatId() {
|
||||
return formatId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// 콤보박스에서 이 객체가 선택될 때 표시될 문자열을 반환합니다.
|
||||
return displayString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class YouTubeDownloaderGUI extends JFrame {
|
||||
|
||||
private JTextField urlField;
|
||||
private JButton downloadButton;
|
||||
private JTextArea outputArea;
|
||||
private JScrollPane scrollPane;
|
||||
private JLabel statusLabel;
|
||||
private JComboBox<FormatItem> formatComboBox;
|
||||
private JButton getFormatsButton;
|
||||
|
||||
private ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
|
||||
public YouTubeDownloaderGUI() {
|
||||
setTitle("YouTube 동영상 다운로더");
|
||||
setSize(1200, 500); // 요청하신 크기로 설정
|
||||
setResizable(false); // 창 크기 고정
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setLocationRelativeTo(null); // 화면 중앙에 배치
|
||||
|
||||
setLayout(new BorderLayout(10, 10));
|
||||
|
||||
JPanel topPanel = new JPanel();
|
||||
topPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
|
||||
topPanel.add(new JLabel("YouTube URL:"));
|
||||
urlField = new JTextField(30);
|
||||
topPanel.add(urlField);
|
||||
|
||||
getFormatsButton = new JButton("화질 정보 가져오기");
|
||||
getFormatsButton.setEnabled(false);
|
||||
topPanel.add(getFormatsButton);
|
||||
|
||||
formatComboBox = new JComboBox<>();
|
||||
formatComboBox.setPreferredSize(new Dimension(150, 25));
|
||||
formatComboBox.setEnabled(false);
|
||||
topPanel.add(new JLabel("화질 선택:"));
|
||||
topPanel.add(formatComboBox);
|
||||
|
||||
downloadButton = new JButton("다운로드 시작");
|
||||
downloadButton.setEnabled(false);
|
||||
topPanel.add(downloadButton);
|
||||
add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
outputArea = new JTextArea();
|
||||
outputArea.setEditable(false);
|
||||
outputArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
|
||||
scrollPane = new JScrollPane(outputArea);
|
||||
add(scrollPane, BorderLayout.CENTER);
|
||||
|
||||
statusLabel = new JLabel("준비 완료");
|
||||
statusLabel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
|
||||
add(statusLabel, BorderLayout.SOUTH);
|
||||
|
||||
getFormatsButton.addActionListener(e -> getAvailableFormats());
|
||||
downloadButton.addActionListener(e -> startDownload());
|
||||
|
||||
checkYoutubeDlInstallation();
|
||||
}
|
||||
|
||||
private void checkYoutubeDlInstallation() {
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
Process process = new ProcessBuilder("yt-dlp", "--version").start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode == 0) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("yt-dlp가 올바르게 설치되었습니다. URL을 입력하고 '화질 정보 가져오기'를 누르세요.");
|
||||
getFormatsButton.setEnabled(true);
|
||||
});
|
||||
} else {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("<html><font color='red'>yt-dlp를 찾을 수 없습니다. 설치 안내를 확인해주세요.</font></html>");
|
||||
getFormatsButton.setEnabled(false);
|
||||
downloadButton.setEnabled(false);
|
||||
outputArea.append("에러: 'yt-dlp' 명령을 찾을 수 없습니다.\n");
|
||||
outputArea.append("yt-dlp가 시스템 PATH에 올바르게 설치되었는지 확인하세요.\n");
|
||||
outputArea.append("설치 방법은 구글 검색을 참고하거나 `pip install yt-dlp`를 실행하세요.\n");
|
||||
});
|
||||
}
|
||||
} catch (IOException | InterruptedException ex) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("<html><font color='red'>yt-dlp 실행 중 오류 발생: " + ex.getMessage() + "</font></html>");
|
||||
getFormatsButton.setEnabled(false);
|
||||
downloadButton.setEnabled(false);
|
||||
outputArea.append("yt-dlp 실행 중 에러가 발생했습니다: " + ex.getMessage() + "\n");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String convertToMiB(String sizeString) {
|
||||
if (sizeString == null || sizeString.trim().isEmpty() || sizeString.equalsIgnoreCase("정보 없음")) {
|
||||
return "정보 없음";
|
||||
}
|
||||
sizeString = sizeString.trim().toLowerCase();
|
||||
|
||||
try {
|
||||
if (sizeString.endsWith("mib")) {
|
||||
return sizeString; // 이미 MiB 단위
|
||||
} else if (sizeString.endsWith("kib")) {
|
||||
double kib = Double.parseDouble(sizeString.replace("kib", ""));
|
||||
return String.format("%.2fMiB", kib / 1024.0);
|
||||
} else if (sizeString.endsWith("gib")) {
|
||||
double gib = Double.parseDouble(sizeString.replace("gib", ""));
|
||||
return String.format("%.2fMiB", gib * 1024.0);
|
||||
} else if (sizeString.endsWith("b")) { // 바이트 단위
|
||||
double bytes = Double.parseDouble(sizeString.replace("b", ""));
|
||||
return String.format("%.2fMiB", bytes / (1024.0 * 1024.0));
|
||||
} else if (sizeString.endsWith("k")) { // 킬로비트 (TBR)
|
||||
// 킬로비트 -> 킬로바이트 ( / 8) -> 메가바이트 ( / 1024)
|
||||
// 대략적인 변환이므로 정확하지 않을 수 있음
|
||||
double kbit = Double.parseDouble(sizeString.replace("k", ""));
|
||||
return String.format("%.2fMiB", (kbit * 1000) / (8.0 * 1024.0 * 1024.0)); // 1kbit = 1000bit, 1MB = 8Mbit
|
||||
} else if (sizeString.matches("^[0-9.]+$")) { // 단위 없는 숫자 (매우 드물지만)
|
||||
double val = Double.parseDouble(sizeString);
|
||||
// 기본 단위를 무엇으로 가정할지 애매하지만, 일단 바이트로 가정
|
||||
return String.format("%.2fMiB", val / (1024.0 * 1024.0));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 파싱 실패 시 원본 문자열 반환 또는 "오류" 표시
|
||||
return "변환 오류";
|
||||
}
|
||||
return sizeString; // 변환할 수 없는 형식
|
||||
}
|
||||
|
||||
|
||||
private void getAvailableFormats() {
|
||||
String videoUrl = urlField.getText().trim();
|
||||
if (videoUrl.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "YouTube URL을 입력해주세요.", "경고", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
outputArea.setText("");
|
||||
formatComboBox.removeAllItems();
|
||||
formatComboBox.setEnabled(false);
|
||||
downloadButton.setEnabled(false);
|
||||
getFormatsButton.setEnabled(false);
|
||||
statusLabel.setText("화질 정보 가져오는 중...");
|
||||
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
String[] command = {"yt-dlp", "--no-warnings", "--list-formats", videoUrl};
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
Process process = pb.start();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String line;
|
||||
List<String> rawFormatLines = new ArrayList<>();
|
||||
boolean formatSectionStarted = false;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final String outputLine = line;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
outputArea.append(outputLine + "\n");
|
||||
scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());
|
||||
});
|
||||
|
||||
if (!formatSectionStarted && line.matches("^\\s*ID\\s+EXT\\s+RESOLUTION.*")) {
|
||||
formatSectionStarted = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (formatSectionStarted && !line.trim().isEmpty() && !line.trim().startsWith("--") && !line.trim().startsWith("[info]") && !line.trim().startsWith("[debug]")) {
|
||||
if (line.matches("^\\s*(\\d+|sb[0-9])\\s+\\S{2,}\\s+.*")) {
|
||||
rawFormatLines.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (exitCode == 0 && !rawFormatLines.isEmpty()) {
|
||||
for (String formatLine : rawFormatLines) {
|
||||
// 그룹 1: ID, 그룹 2: EXT, 그룹 3: RESOLUTION
|
||||
// 그룹 4: FILESIZE (예: 4.36MiB, ~ 5.13MiB, 13.70MiB)
|
||||
// 그룹 5: TBR (예: 130k, 152k)
|
||||
Pattern pattern = Pattern.compile(
|
||||
"^\\s*(\\S+)\\s+" +
|
||||
"(\\S+)\\s+" +
|
||||
"([0-9x]+|audio only)\\s+" +
|
||||
"(?:FPS\\s+\\S+\\s+CH\\s+\\S+\\s+\\u2502\\s*)?" +
|
||||
"(?:~\\s*)?([0-9.]+[GMK]?i?B)?\\s*" + // Filesize (MiB, KiB, GiB)
|
||||
"([0-9.]+[GMK]?k)?.*" // TBR (k)
|
||||
);
|
||||
Matcher matcher = pattern.matcher(formatLine);
|
||||
|
||||
String formatId = "unknown";
|
||||
String ext = "unknown";
|
||||
String resolution = "unknown";
|
||||
String sizeDisplay = "정보 없음"; // MiB로 변환된 최종 용량 문자열
|
||||
|
||||
if (matcher.find()) {
|
||||
formatId = matcher.group(1);
|
||||
ext = matcher.group(2);
|
||||
resolution = matcher.group(3);
|
||||
|
||||
String filesizeRaw = matcher.group(4);
|
||||
String bitrateRaw = matcher.group(5);
|
||||
|
||||
// --- 여기에서 MiB로 변환 로직 호출 ---
|
||||
if (filesizeRaw != null && !filesizeRaw.trim().isEmpty()) {
|
||||
sizeDisplay = convertToMiB(filesizeRaw);
|
||||
} else if (bitrateRaw != null && !bitrateRaw.trim().isEmpty()) {
|
||||
sizeDisplay = convertToMiB(bitrateRaw);
|
||||
} else {
|
||||
// Fallback for cases where main regex didn't catch size/bitrate but they exist
|
||||
Pattern tbrPattern = Pattern.compile("(\\d+\\s*[GMK]?i?B|\\d+\\s*[GMK]?k)");
|
||||
Matcher tbrMatcher = tbrPattern.matcher(formatLine);
|
||||
if (tbrMatcher.find()) {
|
||||
sizeDisplay = convertToMiB(tbrMatcher.group(1));
|
||||
}
|
||||
}
|
||||
// --- MiB 변환 로직 끝 ---
|
||||
|
||||
String displayString;
|
||||
if (resolution.equalsIgnoreCase("audio") || resolution.equalsIgnoreCase("audio only")) {
|
||||
displayString = "오디오 (" + ext + ", " + sizeDisplay + ")";
|
||||
} else {
|
||||
String displayResolution = resolution;
|
||||
if (resolution.contains("x")) {
|
||||
String[] resParts = resolution.split("x");
|
||||
if (resParts.length == 2) {
|
||||
displayResolution = resParts[1] + "p";
|
||||
} else {
|
||||
displayResolution = resolution;
|
||||
}
|
||||
}
|
||||
displayString = displayResolution + " (" + ext + ", " + sizeDisplay + ")";
|
||||
}
|
||||
formatComboBox.addItem(new FormatItem(displayString, formatId));
|
||||
} else {
|
||||
// 정규 표현식 매칭에 실패하더라도, 최소한의 정보로 FormatItem 추가 시도 (fallback)
|
||||
String[] parts = formatLine.trim().split("\\s+");
|
||||
if (parts.length > 1) {
|
||||
String fallbackId = parts[0];
|
||||
String fallbackExt = parts[1];
|
||||
String fallbackResolution = (parts.length > 2) ? parts[2] : "알 수 없는 해상도";
|
||||
String fallbackSizeRaw = "정보 없음"; // 원본 문자열
|
||||
|
||||
// Fallback에서 용량 정보 찾기 (MiB, KiB, GiB, k 포함)
|
||||
for(int i = 0; i < parts.length; i++) {
|
||||
String part = parts[i].toLowerCase();
|
||||
if (part.matches("^[0-9.]+[mgk]?i?b$") || part.matches("^[0-9.]+[mgk]?k$")) {
|
||||
fallbackSizeRaw = parts[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// --- 여기에서 MiB로 변환 로직 호출 (Fallback용) ---
|
||||
String fallbackSizeDisplay = convertToMiB(fallbackSizeRaw);
|
||||
// --- MiB 변환 로직 끝 ---
|
||||
|
||||
String displayResolution = fallbackResolution;
|
||||
if (fallbackResolution.contains("x")) {
|
||||
String[] resParts = fallbackResolution.split("x");
|
||||
if (resParts.length == 2) {
|
||||
displayResolution = resParts[1] + "p";
|
||||
}
|
||||
} else if (fallbackResolution.equalsIgnoreCase("audio") || fallbackResolution.equalsIgnoreCase("audio only")) {
|
||||
displayResolution = "오디오";
|
||||
}
|
||||
|
||||
String fallbackDisplay = displayResolution + " (" + fallbackExt + ", " + fallbackSizeDisplay + ")";
|
||||
formatComboBox.addItem(new FormatItem(fallbackDisplay, fallbackId));
|
||||
} else {
|
||||
formatComboBox.addItem(new FormatItem(formatLine, "unknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
formatComboBox.setEnabled(true);
|
||||
if (formatComboBox.getItemCount() > 0) {
|
||||
downloadButton.setEnabled(true);
|
||||
}
|
||||
statusLabel.setText("화질 정보를 가져왔습니다. 원하는 화질을 선택하세요.");
|
||||
} else if (exitCode != 0) {
|
||||
statusLabel.setText("<html><font color='red'>화질 정보 가져오기 실패. 에러 코드: " + exitCode + "</font></html>");
|
||||
JOptionPane.showMessageDialog(this, "화질 정보를 가져오는 데 실패했습니다. URL을 확인하거나 yt-dlp 출력을 확인하세요.", "오류", JOptionPane.ERROR_MESSAGE);
|
||||
downloadButton.setEnabled(false);
|
||||
} else {
|
||||
statusLabel.setText("<html><font color='orange'>사용 가능한 화질을 찾을 수 없습니다.</font></html>");
|
||||
JOptionPane.showMessageDialog(this, "해당 동영상에서 다운로드할 수 있는 화질이 없습니다.", "정보", JOptionPane.INFORMATION_MESSAGE);
|
||||
downloadButton.setEnabled(false);
|
||||
}
|
||||
getFormatsButton.setEnabled(true);
|
||||
});
|
||||
|
||||
} catch (IOException | InterruptedException ex) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("<html><font color='red'>화질 정보 가져오기 중 오류 발생: " + ex.getMessage() + "</font></html>");
|
||||
outputArea.append("화질 정보 가져오기 중 오류가 발생했습니다: " + ex.getMessage() + "\n");
|
||||
getFormatsButton.setEnabled(true);
|
||||
downloadButton.setEnabled(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startDownload() {
|
||||
String videoUrl = urlField.getText().trim();
|
||||
if (videoUrl.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "YouTube URL을 입력해주세요.", "경고", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
FormatItem selectedFormatItem = (FormatItem) formatComboBox.getSelectedItem();
|
||||
|
||||
if (selectedFormatItem == null) {
|
||||
JOptionPane.showMessageDialog(this, "다운로드할 화질을 먼저 선택해주세요. '화질 정보 가져오기' 버튼을 클릭하세요.", "경고", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
final String formatId = selectedFormatItem.getFormatId();
|
||||
|
||||
if (formatId == null || formatId.isEmpty() || formatId.equals("unknown")) {
|
||||
JOptionPane.showMessageDialog(this, "선택된 화질 항목에서 유효한 Format ID를 찾을 수 없습니다.\n다시 시도하거나 다른 화질을 선택해주세요.", "오류", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
String downloadDir = "C:\\youtube-downloader";
|
||||
File dir = new File(downloadDir);
|
||||
|
||||
if (!dir.exists()) {
|
||||
if (dir.mkdirs()) {
|
||||
} else {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("<html><font color='red'>다운로드 폴더 생성 실패: " + downloadDir + "</font></html>");
|
||||
outputArea.append("에러: 다운로드 폴더를 생성할 수 없습니다. 경로를 확인하거나 권한을 부여하세요.\n");
|
||||
downloadButton.setEnabled(true);
|
||||
JOptionPane.showMessageDialog(this, "다운로드 폴더 생성에 실패했습니다: " + downloadDir, "오류", JOptionPane.ERROR_MESSAGE);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
outputArea.setText("");
|
||||
statusLabel.setText("다운로드 중...");
|
||||
downloadButton.setEnabled(false);
|
||||
getFormatsButton.setEnabled(false);
|
||||
formatComboBox.setEnabled(false);
|
||||
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
String outputPath = downloadDir + File.separator + "%(title)s.%(ext)s";
|
||||
String[] command = {"yt-dlp", "--no-warnings", "-f", formatId, "-o", outputPath, videoUrl};
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
final String outputLine = line;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
outputArea.append(outputLine + "\n");
|
||||
scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());
|
||||
});
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (exitCode == 0) {
|
||||
statusLabel.setText("다운로드 완료!");
|
||||
JOptionPane.showMessageDialog(this, "동영상 다운로드가 완료되었습니다!", "알림", JOptionPane.INFORMATION_MESSAGE);
|
||||
} else {
|
||||
statusLabel.setText("<html><font color='red'>다운로드 실패. 에러 코드: " + exitCode + "</font></html>");
|
||||
JOptionPane.showMessageDialog(this, "동영상 다운로드에 실패했습니다. 콘솔 출력을 확인하세요.", "오류", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
downloadButton.setEnabled(true);
|
||||
getFormatsButton.setEnabled(true);
|
||||
formatComboBox.setEnabled(true);
|
||||
});
|
||||
|
||||
} catch (IOException | InterruptedException ex) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
statusLabel.setText("<html><font color='red'>오류 발생: " + ex.getMessage() + "</font></html>");
|
||||
outputArea.append("다운로드 중 오류가 발생했습니다: " + ex.getMessage() + "\n");
|
||||
downloadButton.setEnabled(true);
|
||||
getFormatsButton.setEnabled(true);
|
||||
formatComboBox.setEnabled(true);
|
||||
JOptionPane.showMessageDialog(this, "다운로드 중 오류가 발생했습니다: " + ex.getMessage(), "오류", JOptionPane.ERROR_MESSAGE);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
new YouTubeDownloaderGUI().setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user