ytdlp 자동 다운로드 및 업데이트 로직 구현

This commit is contained in:
2025-08-30 02:51:58 +09:00
parent 177221bf73
commit 0bff043d6e
36 changed files with 351 additions and 1625 deletions

View File

@@ -1,67 +0,0 @@
#pragma once
#include <dpp/dpp.h>
#include <Queue/MusicQueue.hpp>
#include <condition_variable>
namespace bumbleBee {
class MusicPlayManager {
public:
MusicPlayManager(std::shared_ptr<dpp::cluster> cluster, std::vector<dpp::snowflake> GIDs) :
cluster(cluster), GIDs(GIDs) {
queueMap = std::unordered_map<dpp::snowflake, std::shared_ptr<MusicQueue>>();
queueEmptyMutex = std::unordered_map<dpp::snowflake, std::shared_ptr<std::mutex>>();
cluster->on_voice_ready([this](const dpp::voice_ready_t &event){on_voice_ready(event);});
cluster->on_voice_track_marker([this](const dpp::voice_track_marker_t &event){on_voice_track_marker(event);});
cluster->on_voice_client_disconnect([this](const dpp::voice_client_disconnect_t& event){on_voice_client_disconnect(event);});
for (auto GID : GIDs) {
queueMap[GID] = std::make_shared<MusicQueue>();
queueEmptyMutex[GID] = std::make_shared<std::mutex>();
}
}
/**
* @brief voice_ready 이벤트 인지시 콜백되는 함수
* @param event const dpp::voice_ready_t&
**/
void on_voice_ready(const dpp::voice_ready_t& event);
/**
* @brief voice_track_marker 이벤트 인지시 콜백되는 함수
* @param event const dpp::voice_track_marker_t&
**/
void on_voice_track_marker(const dpp::voice_track_marker_t& event);
/**
* @brief voice_client_disconnect 이벤트 인지시 콜백되는 함수
* @param event const dpp::voice_client_disconnect_t&
**/
void on_voice_client_disconnect(const dpp::voice_client_disconnect_t& event);
void play(dpp::discord_voice_client* client);
void queue_music(const dpp::snowflake guildId, const std::shared_ptr<MusicQueueElement> music);
void clear(const dpp::snowflake guildId);
std::shared_ptr<MusicQueueElement> remove(const dpp::snowflake guildId, dpp::discord_voice_client* client, int index);
int size(const dpp::snowflake guildId);
void setRepeat(const dpp::snowflake guildId, const bool value);
bool getRepeat(const dpp::snowflake guildId);
std::pair<std::shared_ptr<std::list<std::shared_ptr<MusicQueueElement>>>, std::list<std::shared_ptr<MusicQueueElement>>::iterator> getQueue(const dpp::snowflake guildId);
MusicQueueElement getNowPlaying(const dpp::snowflake guildId);
std::condition_variable queuedCondition;
private:
std::shared_ptr<dpp::cluster> cluster;
std::vector<dpp::snowflake> GIDs;
/// @brief 음악 큐
std::unordered_map<dpp::snowflake, std::shared_ptr<MusicQueue>> queueMap;
std::unordered_map<dpp::snowflake, std::shared_ptr<std::mutex>> queueEmptyMutex;
void send_audio_to_voice(std::shared_ptr<bumbleBee::MusicQueueElement> music, dpp::discord_voice_client* client);
};
}

View File

@@ -1,18 +0,0 @@
#pragma once
#include <memory>
#include <dpp/dpp.h>
#include <Queue/MusicQueueElement.hpp>
#include "Utils/ThreadPool.hpp"
namespace bumbleBee {
class ThreadManager : public ThreadPool<dpp::snowflake, int, int> {
public:
bool addMusic(std::shared_ptr<bumbleBee::MusicQueueElement> music, dpp::discord_voice_client* client);
void stopSending(dpp::snowflake gid);
private:
// GID, 쓰레드
std::unordered_map<dpp::snowflake, std::thread> threadPool;
// GID, 쓰레드 종료
std::unordered_map<dpp::snowflake, bool> terminating;
};
}

View File

@@ -1,54 +0,0 @@
#pragma once
#ifndef _BUMBLEBEE_HPP_
#define _BUMBLEBEE_HPP_
#include <dpp/dpp.h>
#include <dpp/nlohmann/json.hpp>
#include <memory>
#include <Commands/BumbleBeeCommand.hpp>
namespace bumbleBee {
/**
* @file BumbleBee.hpp
* @brief 메인 봇 클래스
**/
class BumbleBee {
public:
/**
* @brief 생성자
**/
BumbleBee();
/**
* @brief 파괴자
* @details BumbleBee의 모든 Property를 책임지고 파괴합니다
**/
~BumbleBee() {}
/**
* @brief 봇 시작
**/
void start();
/**
* @brief slashcommand 이벤트 인지시 콜백되는 함수
* @param event const dpp::slashcommand_t&
**/
void on_slashcommand(const dpp::slashcommand_t& event);
/**
* @brief ready 이벤트 인지시 콜백되는 함수
* @param event const dpp::ready_t&
**/
void on_ready(const dpp::ready_t& event);
/// @brief DPP 기본 클러스터 객체
std::shared_ptr<dpp::cluster> cluster;
/// @brief guild id 배열
std::vector<dpp::snowflake> GIDs;
private:
/// @brief Command 목록
std::unordered_map<std::string, std::shared_ptr<commands::ICommand>> commands;
/// @brief voiceclient 관련 event 처리기 <guild id, 각 길드별 MusicPlayManager 인스턴스>
std::shared_ptr<MusicPlayManager> musicManager;
};
}
#endif

View File

@@ -1,69 +0,0 @@
#pragma once
#include <dpp/dpp.h>
#include <Audio/MusicPlayManager.hpp>
#include <functional>
namespace bumbleBee::commands {
class ICommand : public dpp::slashcommand {
public:
/**
* @brief 기본 생성자
* @param botID 봇 아이디
* @param manager 음악재생 매니저
**/
ICommand(dpp::snowflake botID, std::shared_ptr<MusicPlayManager> manager) {
this->botID = botID;
this->musicManager = manager;
}
/**
* @brief 명령어 호출 시에 콜백될 메소드
* @param event dpp::slashcommand_t
**/
virtual void execute(const dpp::slashcommand_t &event){};
/// @brief 명령어 별명
std::vector<std::string> aliases;
private:
/// @brief 봇 ID
dpp::snowflake botID;
protected:
/// @brief 음악재생 매니저
std::shared_ptr<MusicPlayManager> musicManager;
protected:
/// @brief concrete class에서 구현해야 하는 init 이벤트트
virtual void init() = 0;
};
}
/**
* @brief 명령어 인자가 없는 명령어의 boilerplate 대체 매크로
* @param name 명령어 이름 및 클래스명
* @param description 명령어 설명
**/
#define _DECLARE_BUMBLEBEE_COMMAND(CLASS, NAME, DESCRIPTION) \
namespace bumbleBee::commands { \
class CLASS : public ICommand { \
public: \
CLASS (dpp::snowflake botID, std::shared_ptr<MusicPlayManager> manager) \
: ICommand(botID, manager) { \
name = #NAME; \
description = DESCRIPTION; \
init(); \
} \
void execute(const dpp::slashcommand_t &event) override; \
\
protected: \
void init() override; \
}; \
}
_DECLARE_BUMBLEBEE_COMMAND(Delete, d, "큐의 해당하는 번호의 노래를 지웁니다")
_DECLARE_BUMBLEBEE_COMMAND(Leave, l, "음성 채팅방을 떠납니다")
_DECLARE_BUMBLEBEE_COMMAND(Play, p, "노래 재생")
_DECLARE_BUMBLEBEE_COMMAND(Queue, q, "노래 예약 큐를 확인합니다")
_DECLARE_BUMBLEBEE_COMMAND(Repeat, r, "반복을 켜거나 끕니다")
_DECLARE_BUMBLEBEE_COMMAND(Skip, s, "현재 재생중인 곡을 스킵합니다")
_DECLARE_BUMBLEBEE_COMMAND(Shuffle, shuffle, "큐를 섞습니다")

View File

@@ -1,51 +0,0 @@
#pragma once
#include <memory>
#include <functional>
#include <condition_variable>
#include <list>
#include <dpp/dpp.h>
#include <Queue/MusicQueueElement.hpp>
namespace bumbleBee {
class MusicQueue {
public:
MusicQueue() {
queue = std::list<std::shared_ptr<MusicQueueElement>>();
currentPlayingPosition = queue.begin();
repeat = true;
}
void
enqueue(std::shared_ptr<MusicQueueElement> Element);
std::shared_ptr<MusicQueueElement>
dequeue();
std::list<std::shared_ptr<MusicQueueElement>>::iterator
findById(std::string id);
std::list<std::shared_ptr<MusicQueueElement>>::iterator
findByIndex(int index);
std::shared_ptr<MusicQueueElement>
nowplaying();
std::list<std::shared_ptr<MusicQueueElement>>::iterator
next_music();
std::shared_ptr<MusicQueueElement>
jump_to_index(int idx);
void
clear();
std::shared_ptr<MusicQueueElement>
erase(std::list<std::shared_ptr<MusicQueueElement>>::iterator it);
std::pair<std::shared_ptr<std::list<std::shared_ptr<MusicQueueElement>>>, std::list<std::shared_ptr<MusicQueueElement>>::iterator>
getQueueCopy();
int
size();
std::list<std::shared_ptr<MusicQueueElement>>::iterator
end();
bool repeat;
std::list<std::shared_ptr<MusicQueueElement>>::iterator currentPlayingPosition;
private:
std::list<std::shared_ptr<MusicQueueElement>> queue;
std::mutex queueMutex;
};
}

View File

@@ -1,20 +0,0 @@
#pragma once
#include <dpp/dpp.h>
namespace bumbleBee {
class MusicQueueElement {
public:
MusicQueueElement(
std::string id,
std::string query,
dpp::user issuingUser,
dpp::embed embed) :
id(id), query(query), issuingUser(issuingUser), embed(embed) {}
const std::string id;
const std::string query;
const dpp::user issuingUser;
const dpp::embed embed;
};
}

View File

@@ -1,34 +0,0 @@
#pragma once
#include <dpp/dpp.h>
#define _DECLARE_DEFAULT_ACCESSER_STATIC_VARIABLE(type, name, Name)\
private:\
static type name;\
public:\
static type get##Name() {return name;}\
static void set##Name(const type& value) {name = value; saveToFile();}
namespace bumbleBee {
/// @brief 모든 설정은 이 객체를 통해 스태틱하게 제공됨.
class SettingsManager {
/// @brief 봇 토큰
_DECLARE_DEFAULT_ACCESSER_STATIC_VARIABLE(std::string, TOKEN, TOKEN)
/// @brief yt-dlp 실행 명령어
_DECLARE_DEFAULT_ACCESSER_STATIC_VARIABLE(std::string, YTDLP_CMD, YTDLP_CMD)
/// @brief ffmpeg 실행 명령어
_DECLARE_DEFAULT_ACCESSER_STATIC_VARIABLE(std::string, FFMPEG_CMD, FFMPEG_CMD)
/// @brief 로그레벨
_DECLARE_DEFAULT_ACCESSER_STATIC_VARIABLE(dpp::loglevel, LOGLEVEL, LOGLEVEL)
/// @brief 이전에 있던 명령을 지우고 등록할지 선택합니다.
_DECLARE_DEFAULT_ACCESSER_STATIC_VARIABLE(bool, REGISTER_COMMAND, REGISTER_COMMAND)
public:
/// @brief 설정 로드하기, 설정은 이 load()를 부르기 전까지는 적절하지 못한 값을 가지고 있음.
/// @return 로드 성공 시 true, 실패 시 false 반환.
static bool load();
/// @brief 설정 변경사항 저장
static void saveToFile();
/// @brief 토큰이 유효한지 체크합니다.
/// @return 유효한 토큰이면 true, 아니면 false를 반환합니다.
static bool validateToken();
};
}

View File

@@ -1,56 +0,0 @@
#pragma once
#include <queue>
#include <string>
#include <thread>
#include <condition_variable>
#include <dpp/dpp.h>
#include <Queue/MusicQueue.hpp>
namespace bumbleBee {
/// @brief 싱글톤 멀티스레딩 다운로드 매니저
class [[deprecated]] AsyncDownloadManager {
public:
static AsyncDownloadManager& getInstance(int worker_count, std::weak_ptr<dpp::cluster> weak_cluster) {
static AsyncDownloadManager dl(worker_count);
dl.weak_cluster = weak_cluster;
return dl;
}
void enqueue(std::pair<std::string, dpp::message> query) {
std::thread th(&bumbleBee::AsyncDownloadManager::enqueueAsyncDL, this, query);
th.detach();
}
void stop() {
auto cluster = weak_cluster.lock();
cluster->log(dpp::ll_info, "AsyncDownloadManager stop/destructor called.");
terminate = true;
dlQueueCondition.notify_all();
for (auto& t : worker_thread) {
t.join();
}
}
~AsyncDownloadManager(){
stop();
}
private:
AsyncDownloadManager(int worker_count){
worker_thread.reserve(worker_count);
terminate = false;
for (int i=0; i<worker_count; i++) {
worker_thread.emplace_back([this](){this->downloadWorker();});
}
}
void enqueueAsyncDL(std::pair<std::string, dpp::message> query);
void downloadWorker();
std::queue<std::pair<std::string, dpp::message>> downloadQueue;
std::condition_variable dlQueueCondition;
std::mutex dlQueueMutex;
std::weak_ptr<dpp::cluster> weak_cluster;
std::vector<std::thread> worker_thread;
bool terminate;
};
}

View File

@@ -1,46 +0,0 @@
#pragma once
#include <iostream>
#include <sstream>
#include <queue>
#include <regex>
#include <boost/process.hpp>
namespace bumbleBee {
class ConsoleUtils {
public:
/**
* @brief 명령어를 쉘에서 실행하고 결과를 EOF 전까지 읽어 \n을 구분자로 토큰화하여 반환합니다
* @param cmd 실행할 명령
* @param args 아규먼트
* @return std::queue<std::string> tokens
*/
static std::queue<std::string> safe_execute_command(const std::string& cmd, const std::vector<std::string>& args) {
std::queue<std::string> tokens;
try {
boost::process::ipstream output; // 명령의 출력을 받을 스트림
boost::process::child c(cmd, boost::process::args(args), boost::process::std_out > output);
std::string line;
while (!output.eof() && std::getline(output, line))
tokens.push(line);
c.wait(); // 프로세스가 종료될 때까지 대기
return tokens;
} catch (const std::exception& e) {
return tokens;
}
}
/**
* @brief 명령어를 쉘에서 실행하고 결과를 파이프로 연결하여 반환합니다
* @param cmd 실행할 명령
* @param args 아규먼트
* @return FILE* fd
*/
static FILE* safe_open_pipe(const std::string& cmd, const std::vector<std::string>& args) {
boost::process::pipe pipe;
boost::process::child c(cmd, boost::process::args(args), boost::process::std_out > pipe);
return fdopen(pipe.native_source(), "r");
}
};
}

View File

@@ -1,77 +0,0 @@
#pragma once
#include <dpp/dpp.h>
#include <Queue/MusicQueueElement.hpp>
#include <cmath>
namespace bumbleBee {
class QueuedMusicListEmbedProvider {
public:
static std::queue<dpp::embed> makeEmbed(std::shared_ptr<std::list<std::shared_ptr<MusicQueueElement>>> queue, std::list<std::shared_ptr<MusicQueueElement>>::iterator np, bool repeat) {
std::queue<dpp::embed> returnValue;
std::list<std::shared_ptr<MusicQueueElement>>::iterator it = queue->begin();
if (queue->size() == 0) {
dpp::embed embed = makeEmbedPart(queue, np, repeat, it);
returnValue.push(embed);
return returnValue;
}
for (int i = 0; i < std::ceil(queue->size() / 5.0) && it != queue->end(); i++) {
dpp::embed embed = makeEmbedPart(queue, np, repeat, it);
returnValue.push(embed);
}
return returnValue;
}
private:
static dpp::embed makeEmbedPart(
std::shared_ptr<std::list<std::shared_ptr<MusicQueueElement>>> queue,
std::list<std::shared_ptr<MusicQueueElement>>::iterator np,
bool repeat,
std::list<std::shared_ptr<MusicQueueElement>>::iterator &startIter) {
dpp::embed embed = dpp::embed()
.set_color(dpp::colors::sti_blue);
if (queue->begin() == queue->end()) {
embed
.set_title("큐가 비었습니다!")
.set_timestamp(time(0));
if (repeat)
embed.add_field(":repeat:","");
return embed;
}
int startIndex = std::distance(queue->begin(), startIter) + 1;
int index = startIndex;
for (; (index < startIndex + 5) && startIter != queue->end() && index < queue->size()+1; startIter++, index++) { //iter로 순회하면 왠지 모르게 이상한 값을 읽을 때가 있음 이유는 나도 몰?루
if (*startIter == *np)
embed.add_field (
"np",
"",
true
);
else
embed.add_field (
std::to_string(index),
"",
true
);
embed.add_field (
(*startIter)->embed.title,
(*startIter)->embed.description,
true
)
.add_field("","");
}
if (startIter == queue->end() || index >= queue->size()+1) {
embed.set_timestamp(time(0));
if (repeat)
embed.add_field(":repeat:","");
}
return embed;
}
};
}

View File

@@ -1,83 +0,0 @@
#pragma once
#include <dpp/dpp.h>
#include "ConsoleUtils.hpp"
#include "../Settings/SettingsManager.hpp"
namespace bumbleBee {
class VersionsCheckUtils {
public:
static bool isThereCMD(std::shared_ptr<dpp::cluster> cluster, std::string cmd) {
if (ConsoleUtils::safe_execute_command("/usr/bin/which", {cmd}).size() == 0) {
cluster->log(dpp::ll_error, cmd + " is unavaliable. unresolable error please install " + cmd);
return false;
}
else
return true;
}
static void validateFFMPEG(std::shared_ptr<dpp::cluster> cluster) {
std::queue<std::string> result = ConsoleUtils::safe_execute_command(SettingsManager::getFFMPEG_CMD(), {"-version"});
std::string front = result.front();
if (front[0] != 'f' ||
front[1] != 'f' ||
front[2] != 'm' ||
front[3] != 'p' ||
front[4] != 'e' ||
front[5] != 'g') {
cluster->log(dpp::ll_warning, "ffmpeg is unavailable. downloading ffmpeg...");
if (!isThereCMD(cluster, "curl")) {
exit(1);
}
if (!isThereCMD(cluster, "tar")) {
exit(1);
}
system("curl -LO https://github.com/BtbN/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-linux64-gpl.tar.xz");
system("tar -xf ffmpeg-master-latest-linux64-gpl.tar.xz");
system("rm ffmpeg-master-latest-linux64-gpl.tar.xz");
system("mv ffmpeg-master-latest-linux64-gpl/bin/ffmpeg .");
system("rm -rf ffmpeg-master-latest-linux64-gpl");
SettingsManager::setFFMPEG_CMD("./ffmpeg");
}
}
static void validateYTDLP(std::shared_ptr<dpp::cluster> cluster) {
std::queue<std::string> result = ConsoleUtils::safe_execute_command(SettingsManager::getYTDLP_CMD(), {"--version"});
std::string front = result.front();
if ((front[0]-'0' < 0 || front[0]-'0' > 9) ||
(front[1]-'0' < 0 || front[1]-'0' > 9) ||
(front[2]-'0' < 0 || front[2]-'0' > 9) ||
(front[3]-'0' < 0 || front[3]-'0' > 9)) {
cluster->log(dpp::ll_warning, "ytdlp is unavailable. downloading ytdlp...");
if (!isThereCMD(cluster, "curl")) {
exit(1);
}
if (!isThereCMD(cluster, "tar")) {
exit(1);
}
system("curl -LO https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp");
system("chmod +x ./yt-dlp");
SettingsManager::setYTDLP_CMD("./yt-dlp");
}
}
static void validateYTDLPFFMPEGBinary(std::shared_ptr<dpp::cluster> cluster) {
cluster->log(dpp::ll_info, "Checking if yt-dlp and ffmpeg is available...");
validateFFMPEG(cluster);
validateYTDLP(cluster);
}
static void updateytdlp(std::shared_ptr<dpp::cluster> cluster) {
cluster->log(dpp::ll_info, "Checking if yt-dlp update is available...");
std::queue<std::string> result = ConsoleUtils::safe_execute_command("./yt-dlp", {"-U"});
while(!result.empty()) {
std::string front = result.front();
result.pop();
cluster->log(dpp::ll_info, front);
}
}
};
}

23
include/precomp.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef BUMBLEBEE_INCLUDE_PRECOMP_H_
#define BUMBLEBEE_INCLUDE_PRECOMP_H_
#ifdef WIN32
#include <winsock2.h>
#endif
#include <iostream>
#include <queue>
#include "boost/asio.hpp"
#include "boost/beast/http.hpp"
#include "boost/beast/ssl.hpp"
#include "boost/log/trivial.hpp"
#include "boost/process.hpp"
extern "C" {
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"
}
#endif // BUMBLEBEE_INCLUDE_PRECOMP_H_

38
include/utils/console.h Normal file
View File

@@ -0,0 +1,38 @@
#ifndef BUMBLEBEE_INCLUDE_UTILS_CONSOLE_H_
#define BUMBLEBEE_INCLUDE_UTILS_CONSOLE_H_
#include "precomp.h"
namespace utils {
// @brief 명령어가 실행 가능한지 체크합니다
// @param cmd 실행할 명령
// @return int error_code
int ValidateCommand(std::string cmd);
// @brief 명령어를 쉘에서 실행하고 결과를 EOF 전까지 읽어 반환합니다
// @param cmd 실행할 명령
// @param args 아규먼트
// @return int error_code
int ExecuteCommand(
const std::string& cmd,
const std::vector<std::string>& args = std::vector<std::string>());
// @brief 명령어를 쉘에서 실행하고 결과를 EOF 전까지 읽어 반환합니다
// @param cmd 실행할 명령
// @param result 실행결과
// @return int error_code
int ExecuteCommand(const std::string& cmd, std::string& result);
// @brief 명령어를 쉘에서 실행하고 결과를 EOF 전까지 읽어 반환합니다
// @param cmd 실행할 명령
// @param args 아규먼트
// @param result 실행결과
// @return int error_code
int ExecuteCommand(const std::string& cmd, const std::vector<std::string>& args,
std::string& result);
// @brief 명령어를 쉘에서 실행하고 결과를 파이프로 연결하여 반환합니다
// @param cmd 실행할 명령
// @param args 아규먼트
// @return boost::process::popen
boost::process::popen OpenPipe(
const std::string& cmd,
const std::vector<std::string>& args = std::vector<std::string>());
} // namespace utils
#endif

View File

@@ -0,0 +1,10 @@
#ifndef BUMBLEBEE_INCLUDE_UTILS_FILE_DOWNLOADER_H_
#define BUMBLEBEE_INCLUDE_UTILS_FILE_DOWNLOADER_H_
#include "precomp.h"
namespace utils {
void DownloadFileFromHTTPS(std::string url, std::string filename);
}
#endif

View File

@@ -0,0 +1,14 @@
#ifndef BUMBLEBEE_INCLUDE_UTILS_UPDATE_CHECKER_H_
#define BUMBLEBEE_INCLUDE_UTILS_UPDATE_CHECKER_H_
#include "precomp.h"
namespace utils {
int InstallYtdlp();
int CheckUpdate();
} // namespace utils
#endif