일단은 임시저장, 스트리밍의 실마리를 잡았다

This commit is contained in:
2025-01-30 22:57:01 +09:00
parent 3186561818
commit c2cb16c6d7
19 changed files with 368 additions and 81 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ Temp/
config.json
Music
*.info.json
yt-dlp
yt-dlp
ffmpeg

22
.vscode/settings.json vendored
View File

@@ -77,6 +77,26 @@
"typeinfo": "cpp",
"valarray": "cpp",
"variant": "cpp",
"*.ipp": "cpp"
"*.ipp": "cpp",
"format": "cpp",
"span": "cpp",
"__bit_reference": "cpp",
"__bits": "cpp",
"__config": "cpp",
"__debug": "cpp",
"__errc": "cpp",
"__hash_table": "cpp",
"__locale": "cpp",
"__mutex_base": "cpp",
"__node_handle": "cpp",
"__nullptr": "cpp",
"__split_buffer": "cpp",
"__string": "cpp",
"__threading_support": "cpp",
"__tree": "cpp",
"__tuple": "cpp",
"ios": "cpp",
"locale": "cpp",
"queue": "cpp"
}
}

View File

@@ -6,7 +6,8 @@ set(BOT_NAME "BumbleCee")
project(${BOT_NAME})
aux_source_directory("src" coresrc)
aux_source_directory("src/Commands" commands)
add_executable(${BOT_NAME} ${coresrc} ${commands})
aux_source_directory("src/Settings" settings)
add_executable(${BOT_NAME} ${coresrc} ${commands} ${settings})
string(ASCII 27 Esc)

View File

@@ -5,30 +5,28 @@
#include <string>
#include <thread>
#include <condition_variable>
#include <curl/curl.h>
#include <dpp/dpp.h>
#include <MusicQueue.hpp>
#define WORKER_COUNT 5
namespace bumbleBee {
/// @brief 싱글톤 멀티스레딩 다운로드 매니저
class AsyncDownloadManager {
public:
static AsyncDownloadManager& getInstance(int worker_count, std::weak_ptr<dpp::cluster> bot, std::shared_ptr<bumbleBee::MusicQueue> musicQueue) {
static AsyncDownloadManager& getInstance(int worker_count, std::weak_ptr<dpp::cluster> weak_cluster, std::shared_ptr<bumbleBee::MusicQueue> musicQueue) {
static AsyncDownloadManager dl(worker_count);
dl.bot = bot;
dl.weak_cluster = weak_cluster;
dl.musicQueue = musicQueue;
return dl;
}
void enqueue(std::string query) {
void enqueue(std::pair<std::string, dpp::message> query) {
std::thread th(&bumbleBee::AsyncDownloadManager::enqueueAsyncDL, this, query);
th.detach();
}
~AsyncDownloadManager(){
auto cluster = bot.lock();
assert(cluster);
cluster->log(dpp::ll_info, "AsyncDownloadManager Destructor called.");
void stop() {
auto cluster = weak_cluster.lock();
cluster->log(dpp::ll_info, "AsyncDownloadManager stop/destructor called.");
terminate = true;
dlQueueCondition.notify_all();
@@ -37,6 +35,10 @@ public:
}
}
~AsyncDownloadManager(){
stop();
}
private:
AsyncDownloadManager(int worker_count){
worker_thread.reserve(worker_count);
@@ -46,13 +48,13 @@ private:
}
}
void enqueueAsyncDL(std::string query);
void enqueueAsyncDL(std::pair<std::string, dpp::message> query);
void downloadWorker();
std::queue<std::string> downloadQueue;
std::queue<std::pair<std::string, dpp::message>> downloadQueue;
std::condition_variable dlQueueCondition;
std::mutex dlQueueMutex;
std::weak_ptr<dpp::cluster> bot;
std::weak_ptr<dpp::cluster> weak_cluster;
std::shared_ptr<bumbleBee::MusicQueue> musicQueue;
std::vector<std::thread> worker_thread;
bool terminate;

View File

@@ -18,9 +18,8 @@ public:
/**
* @fn BumbleBee(nlohmann::json settings)
* @brief 생성자
* @param settings 설정 json
**/
BumbleBee(nlohmann::json settings);
BumbleBee();
/**
* @fn ~BumbleBee()
* @brief 파괴자

View File

@@ -7,15 +7,19 @@
namespace bumbleBee::commands {
class ICommand {
public:
/// @brief 기본 생성자
/// @param botID 명령어 등록을 위한 봇 아이디
/// @param queue 음악이 저장되어 있는 큐
/**
* @brief 기본 생성자
* @param botID 명령어 등록을 위한 봇 아이디
* @param queue 음악이 저장되어 있는 큐
**/
ICommand(dpp::snowflake botID, std::weak_ptr<MusicQueue> queue) {
this->botID = botID;
this->queue = queue;
}
/// @brief 명령어 호출 시에 콜백되는 함수
/// @param event
/**
* @brief 명령어 호출 시에 콜백될 메소드
* @param event dpp::slashcommand_t
**/
virtual void operator()(const dpp::slashcommand_t &event){};
/// @brief 봇 명령어들의 이름과 별명들을 저장하는 벡터
@@ -26,11 +30,19 @@ private:
dpp::snowflake botID;
/// @brief 음악 큐에 대한 약한 포인터
std::weak_ptr<MusicQueue> queue;
/**
* @brief 명령어 별명 추가
* @param name 명령어 이름
* @param description 명령어 설명
**/
void addCommandAliase(std::string name, std::string description) {
nameAndAliases.push_back(dpp::slashcommand(name, description, botID));
}
};
}
/**
* @fn _DECLARE_BUMBLEBEE_COMMAND_one_PARAM_one_ALIAS(name, alias, description, option_name, option_desc)
* @brief 명령어 인자가 없는 명령어의 boilerplate 대체 매크로
* @param name 명령어 이름 및 클래스명
* @param alias 명령어 별명
@@ -52,7 +64,6 @@ public: \
}
/**
* @fn _DECLARE_BUMBLEBEE_COMMAND_one_PARAM_one_ALIAS(name, alias, description, option_name, option_desc)
* @brief 명령어 인자를 하나 갖는 명령어의 boilerplate 대체 매크로
* @param name 명령어 이름 및 클래스명
* @param alias 명령어 별명

View File

@@ -0,0 +1,16 @@
#pragma once
#ifndef _MUSICPLAYMANAGER_HPP_
#define _MUSICPLAYMANAGER_HPP_
#include <dpp/dpp.h>
namespace BumbleBee {
class MusicPlayManager {
public:
MusicPlayManager(std::shared_ptr<dpp::cluster> cluster) {
this->cluster = cluster;
}
private:
std::shared_ptr<dpp::cluster> cluster;
};
}
#endif

View File

@@ -5,14 +5,23 @@
#include <functional>
#include <condition_variable>
#include <list>
#include <dpp/dpp.h>
namespace bumbleBee {
class MusicQueueElement {
public:
MusicQueueElement(std::string id, std::string url) : id(id), url(url) {}
MusicQueueElement(
dpp::message originalResponse,
std::string id,
std::string url,
FILE* stream) :
originalResponse(originalResponse), id(id), url(url), stream(stream) {}
dpp::message originalResponse;
std::string id;
std::string url;
FILE* stream;
};
class MusicQueue {
@@ -23,6 +32,7 @@ public:
}
void enqueue(std::shared_ptr<MusicQueueElement> Element);
std::shared_ptr<MusicQueueElement> dequeue();
std::shared_ptr<MusicQueueElement> findById(std::string id);
std::weak_ptr<MusicQueueElement> nowplaying();
std::weak_ptr<MusicQueueElement> next_music();
std::weak_ptr<MusicQueueElement> jump_to_index(int idx);

View File

@@ -0,0 +1,30 @@
#pragma once
#ifndef _SETTINGSMANAGER_HPP_
#define _SETTINGSMANAGER_HPP_
#include <string>
#include <dpp/nlohmann/json.hpp>
#include <fstream>
namespace bumbleBee {
class settingsManager {
public:
/// @brief 봇 토큰
static std::string TOKEN;
/// @brief yt-dlp 실행 명령어
static std::string YTDLP_CMD;
/// @brief ffmpeg 실행 명령어
static std::string FFMPEG_CMD;
/// @brief db접속 url
static std::string DBURL;
/// @brief db접속 유저
static std::string DBUSER;
/// @brief db접속 비밀번호
static std::string DBPASSWORD;
static void load();
static void saveToFile();
};
}
#endif

View File

@@ -0,0 +1,40 @@
#pragma once
#ifndef _CONSOLEUTILS_HPP_
#define _CONSOLEUTILS_HPP_
#include <iostream>
#include <sstream>
#include <queue>
namespace bumbleBee {
class ConsoleUtils {
public:
/**
* @brief 명령어를 쉘에서 실행하고 결과를 EOF 전까지 읽어 \n을 구분자로 토큰화하여 반환합니다
* @param cmd 실행할 명령
* @return std::queue<std::string> tokens
*/
static std::queue<std::string> getResultFromCommand(std::string cmd) {
std::string result, token;
std::queue<std::string> tokens;
FILE* stream;
const int maxBuffer = 12; // 적당한 크기
char buffer[maxBuffer];
cmd.append(" 2>&1"); // 표준에러를 표준출력으로 redirect
stream = popen(cmd.c_str(), "r"); // 주어진 command를 shell로 실행하고 파이프 연결 (fd 반환)
if (stream) {
while (fgets(buffer, maxBuffer, stream) != NULL) result.append(buffer); // fgets: fd (stream)를 길이 (maxBuffer)만큼 읽어 버퍼 (buffer)에 저장
pclose(stream);
}
std::stringstream ss(result);
while (std::getline(ss, token, '\n')) {
tokens.push(token);
}
return tokens;
}
};
}
#endif

View File

@@ -0,0 +1,75 @@
#pragma once
#ifndef _VERSIONCHECKUTILS_HPP_
#define _VERSIONCHECKUTILS_HPP_
#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::getResultFromCommand("which " + cmd).size() == 0) {
cluster->log(dpp::ll_error, cmd + " is unavaliable. unresolable error please install " + cmd);
return false;
}
else
return true;
}
static void validateYTDLPFFMPEGBinary(std::shared_ptr<dpp::cluster> cluster) {
cluster->log(dpp::ll_info, "Checking if yt-dlp and ffmpeg is available...");
std::queue<std::string> result = ConsoleUtils::getResultFromCommand(settingsManager::FFMPEG_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 ffmpeg");
}
result = ConsoleUtils::getResultFromCommand(settingsManager::YTDLP_CMD + " --version");
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");
}
}
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::getResultFromCommand("./yt-dlp -U");
while(!result.empty()) {
std::string front = result.front();
result.pop();
cluster->log(dpp::ll_info, front);
}
}
};
}
#endif

View File

@@ -1,36 +1,20 @@
#include <AsyncDownloadManager.hpp>
#include <sstream>
#include "Utils/ConsoleUtils.hpp"
#include "Settings/SettingsManager.hpp"
#include <ogg/ogg.h>
#include <oggz/oggz.h>
#include <opus/opusfile.h>
#include <memory>
namespace bumbleBee {
void AsyncDownloadManager::enqueueAsyncDL(std::string query) {
void AsyncDownloadManager::enqueueAsyncDL(std::pair<std::string, dpp::message> query) {
std::lock_guard<std::mutex> lock(dlQueueMutex);
downloadQueue.push(query);
dlQueueCondition.notify_one();
}
std::queue<std::string> getResultFromCommand(std::string cmd) {
std::string result, token;
std::queue<std::string> tokens;
FILE* stream;
const int maxBuffer = 12; // 버퍼의 크기는 적당하게
char buffer[maxBuffer];
cmd.append(" 2>&1"); // 표준에러를 표준출력으로 redirect
stream = popen(cmd.c_str(), "r"); // 주어진 command를 shell로 실행하고 파이프 연결 (fd 반환)
if (stream) {
while (fgets(buffer, maxBuffer, stream) != NULL) result.append(buffer); // fgets: fd (stream)를 길이 (maxBuffer)만큼 읽어 버퍼 (buffer)에 저장
pclose(stream);
}
std::stringstream ss(result);
while (std::getline(ss, token, '\n')) {
tokens.push(token);
}
return tokens;
}
void AsyncDownloadManager::downloadWorker() {
std::ostringstream tid;
tid << std::this_thread::get_id();
@@ -38,38 +22,49 @@ void AsyncDownloadManager::downloadWorker() {
//mutex lock
std::unique_lock<std::mutex> dlQueueLock(dlQueueMutex);
dlQueueCondition.wait(dlQueueLock, [&]{ return !downloadQueue.empty() || terminate; });
auto cluster = bot.lock();
assert(cluster);
if (terminate) {
cluster->log(dpp::ll_info, "Terminating Thread" + tid.str());
auto cluster = weak_cluster.lock();
if (weak_cluster.expired()) {
cluster->log(dpp::ll_error, "Missing cluster, terminating thread " + tid.str());
break;
}
std::string query = downloadQueue.front();
if (terminate) {
cluster->log(dpp::ll_info, "Terminating thread " + tid.str());
break;
}
std::string query = downloadQueue.front().first;
dpp::message oRes = downloadQueue.front().second;
downloadQueue.pop();
dlQueueLock.unlock();
cluster->log(dpp::ll_info, "Enqueuing " + query + " accepted.");
cluster->log(dpp::ll_info, "AsyncDownloadManager: " + query + " accepted.");
std::queue<std::string> ids = getResultFromCommand("./yt-dlp --default-search ytsearch --flat-playlist --skip-download --quiet --ignore-errors --print id " + query);
std::queue<std::string> ids =
ConsoleUtils::getResultFromCommand(settingsManager::YTDLP_CMD +
" --default-search ytsearch --flat-playlist --skip-download --quiet --ignore-errors --print id " + query);
if (ids.size() >= 2) {
cluster->log(dpp::ll_info, query + " is playlist");
while (!ids.empty()) {
if (ids.front() == "") {
ids.pop();
continue;
}
cluster->log(dpp::ll_info, "Enqueuing " + ids.front());
enqueue("https://youtu.be/" + ids.front());
cluster->log(dpp::ll_info, "Enqueuing playlist element " + ids.front());
enqueue(std::make_pair("https://youtu.be/" + ids.front(), oRes));
ids.pop();
}
break;
}
std::queue<std::string> urls = getResultFromCommand("./yt-dlp -f ba* --print urls https://youtu.be/" + ids.front());
std::queue<std::string> urls =
ConsoleUtils::getResultFromCommand(settingsManager::YTDLP_CMD +
" -f ba* --print urls https://youtu.be/" + ids.front());
cluster->log(dpp::ll_info, "url: " + urls.front());
cluster->log(dpp::ll_debug, "url: " + urls.front());
musicQueue->enqueue(std::make_shared<MusicQueueElement>(ids.front(), urls.front()));
FILE* stream;
musicQueue->enqueue(std::make_shared<MusicQueueElement>(oRes, ids.front(), urls.front(), stream));
std::string downloadID = ids.front();
@@ -81,13 +76,12 @@ void AsyncDownloadManager::downloadWorker() {
cluster->log(dpp::ll_info, "Thread id: " + tid.str() + ": " + downloadID + " accepted.");
system(("./yt-dlp -o \"Temp/%(id)s\" --quiet --ignore-errors -f ba* https://youtu.be/" + downloadID).c_str());
std::string command = std::string("./streamAndDownload.sh " + settingsManager::YTDLP_CMD + " " + downloadID + " " + settingsManager::FFMPEG_CMD);
stream = popen(command.c_str(), "r");
pclose(stream);
stream = NULL;
system((std::string() + "yes n 2>/dev/null | ffmpeg -hide_banner -loglevel error -i \""
+ "Temp/" + downloadID + "\" -acodec libopus -vn Music/" + downloadID + ".ogg").c_str());
system((std::string() + "rm -f Temp/" + downloadID).c_str());
cluster->log(dpp::ll_info, "Thread id: " + tid.str() + ": " + downloadID + " downloaded.");
cluster->log(dpp::ll_info, "Thread id: " + tid.str() + ": " + downloadID + " download complete.");
});
th.detach();
}

View File

@@ -1,13 +1,17 @@
#include <BumbleBee.hpp>
#include <memory>
#include <Settings/SettingsManager.hpp>
#include <Utils/VersionsCheckUtils.hpp>
namespace bumbleBee{
BumbleBee::BumbleBee(nlohmann::json settings) {
this->cluster = std::make_shared<dpp::cluster>(settings["token"]);
BumbleBee::BumbleBee() {
settingsManager::load();
this->cluster = std::make_shared<dpp::cluster>(settingsManager::TOKEN);
dbDriver = sql::mariadb::get_driver_instance();
this->dbURL = std::make_shared<sql::SQLString>(settings["dbURL"]);
this->dbURL = std::make_shared<sql::SQLString>(settingsManager::DBURL);
sql::Properties pro({
{"user", std::string(settings["user"])},
{"password", std::string(settings["password"])}
{"user", std::string(settingsManager::DBUSER)},
{"password", std::string(settingsManager::DBPASSWORD)}
});
this->dbProperties = std::make_shared<sql::Properties>(pro);
@@ -16,6 +20,9 @@ BumbleBee::BumbleBee(nlohmann::json settings) {
cluster->on_ready([this](const dpp::ready_t &event){on_ready(event);});
queue = std::make_shared<MusicQueue>();
VersionsCheckUtils::validateYTDLPFFMPEGBinary(cluster);
VersionsCheckUtils::updateytdlp(cluster);
}
void BumbleBee::start() { this->cluster->start(dpp::st_wait); }

View File

@@ -14,7 +14,17 @@ std::shared_ptr<MusicQueueElement> MusicQueue::dequeue() {
queue.pop_front();
return value;
}
std::weak_ptr<MusicQueueElement> MusicQueue::nowplaying() {
std::shared_ptr<MusicQueueElement> MusicQueue::findById(std::string id) {
std::lock_guard<std::mutex> lock(queueMutex);
int index = 0;
for (auto iter = queue.begin(); iter != queue.end(); iter++) {
if ((*iter).get()->id == id)
return *iter;
}
return std::shared_ptr<MusicQueueElement>();
}
std::weak_ptr<MusicQueueElement> MusicQueue::nowplaying()
{
return *currentPlayingPosition;
}
std::weak_ptr<MusicQueueElement> MusicQueue::next_music() {
@@ -38,7 +48,6 @@ std::weak_ptr<MusicQueueElement> MusicQueue::jump_to_index(int idx) {
return *iter;
}
}
std::shared_ptr<MusicQueueElement> empty;
return empty;
return std::shared_ptr<MusicQueueElement>();
}
}

View File

@@ -0,0 +1,67 @@
#include <Settings/SettingsManager.hpp>
namespace bumbleBee {
std::string settingsManager::TOKEN = "";
std::string settingsManager::YTDLP_CMD = "./yt-dlp";
std::string settingsManager::FFMPEG_CMD = "./ffmpeg/bin/ffmpeg";
std::string settingsManager::DBURL = "";
std::string settingsManager::DBUSER = "";
std::string settingsManager::DBPASSWORD = "";
void settingsManager::load() {
nlohmann::json configdocument;
try {
std::ifstream configfile("config.json");
configfile >> configdocument;
TOKEN = configdocument["TOKEN"];
YTDLP_CMD = configdocument["YTDLP_CMD"];
FFMPEG_CMD = configdocument["FFMPEG_CMD"];
DBURL = configdocument["DBURL"];
DBUSER = configdocument["DBUSER"];
DBPASSWORD = configdocument["DBPASSWORD"];
}
catch (const nlohmann::json::type_error& e) {
saveToFile();
std::ifstream configfile("config.json");
configfile >> configdocument;
TOKEN = configdocument["TOKEN"];
YTDLP_CMD = configdocument["YTDLP_CMD"];
FFMPEG_CMD = configdocument["FFMPEG_CMD"];
DBURL = configdocument["DBURL"];
DBUSER = configdocument["DBUSER"];
DBPASSWORD = configdocument["DBPASSWORD"];
}
catch (const nlohmann::json::parse_error& e) {
saveToFile();
std::ifstream configfile("config.json");
configfile >> configdocument;
TOKEN = configdocument["TOKEN"];
YTDLP_CMD = configdocument["YTDLP_CMD"];
FFMPEG_CMD = configdocument["FFMPEG_CMD"];
DBURL = configdocument["DBURL"];
DBUSER = configdocument["DBUSER"];
DBPASSWORD = configdocument["DBPASSWORD"];
}
}
void settingsManager::saveToFile() {
nlohmann::json configdocument;
configdocument["TOKEN"] = TOKEN;
configdocument["YTDLP_CMD"] = YTDLP_CMD;
configdocument["FFMPEG_CMD"] = FFMPEG_CMD;
configdocument["DBURL"] = DBURL;
configdocument["DBUSER"] = DBUSER;
configdocument["DBPASSWORD"] = DBPASSWORD;
std::ofstream configFile("config.json");
if (configFile.is_open()) {
configFile << configdocument.dump(4);
configFile.close();
}
}
}

View File

@@ -4,17 +4,14 @@
#include <thread>
int main() {
nlohmann::json configdocument;
std::ifstream configfile("config.json");
configfile >> configdocument;
bumbleBee::BumbleBee bot(configdocument);
bumbleBee::BumbleBee bot;
bumbleBee::AsyncDownloadManager& manager = bumbleBee::AsyncDownloadManager::getInstance(5, bot.cluster, bot.queue);
manager.enqueue("https://music.youtube.com/playlist?list=PL5NSTAfQ-wQBqZYMTqxADemyUW8mxJq2h&si=vFV4jlm70kxGfKNa");
manager.enqueue(std::make_pair("https://music.youtube.com/watch?v=4NnqIu_v1QA&si=buZP2UwzQtJLENmb", nullptr));
std::thread th([](){sleep(11);});
std::thread th([](){sleep(60);});
th.join();
manager.stop();
std::cout << "\n\n\n\n\nend\n\n\n\n\n\n\n";
//bot.start();

8
streamAndDownload.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
YTDLP_CMD=$1
DOWNLOAD_ID=$2
FFMPEG_CMD=$3
$YTDLP_CMD -o - --quiet --ignore-errors -f bestaudio https://youtu.be/$DOWNLOAD_ID | \
$FFMPEG_CMD -i - -hide_banner -ar 48000 -ac 2 -c:a libopus -f ogg -

0
test.ogg Normal file
View File

BIN
yt-dlp

Binary file not shown.