This commit is contained in:
parent
9876641ff6
commit
9e985d05f1
11 changed files with 1011 additions and 70 deletions
|
|
@ -22,6 +22,147 @@
|
|||
using namespace drogon::orm;
|
||||
|
||||
namespace {
|
||||
// Generate waveform data using ffmpeg (safe version)
|
||||
// Returns path to waveform JSON file, or empty string on failure
|
||||
std::string generateWaveform(const std::string& audioPath, int64_t audioId) {
|
||||
if (!isPathSafe(audioPath, "/app/uploads")) {
|
||||
LOG_ERROR << "Unsafe audio path rejected for waveform: " << audioPath;
|
||||
return "";
|
||||
}
|
||||
|
||||
// Ensure waveforms directory exists
|
||||
const std::string waveformDir = "/app/uploads/audio/waveforms";
|
||||
if (!ensureDirectoryExists(waveformDir)) {
|
||||
LOG_ERROR << "Failed to create waveforms directory";
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string waveformPath = waveformDir + "/" + std::to_string(audioId) + ".json";
|
||||
|
||||
// Use ffmpeg to extract raw audio samples (mono, downsampled to 8000Hz)
|
||||
// Then we'll compute peaks from the samples
|
||||
std::vector<std::string> args = {
|
||||
"/usr/bin/ffmpeg", "-i", audioPath,
|
||||
"-ac", "1", // mono
|
||||
"-ar", "8000", // 8kHz sample rate
|
||||
"-f", "f32le", // 32-bit float, little endian
|
||||
"-acodec", "pcm_f32le",
|
||||
"-v", "error",
|
||||
"-y", // overwrite
|
||||
"pipe:1" // output to stdout
|
||||
};
|
||||
|
||||
// Execute ffmpeg and capture raw samples
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) == -1) {
|
||||
LOG_ERROR << "Failed to create pipe for waveform generation";
|
||||
return "";
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
LOG_ERROR << "Failed to fork for waveform generation";
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return "";
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// Child process
|
||||
close(pipefd[0]); // Close read end
|
||||
dup2(pipefd[1], STDOUT_FILENO);
|
||||
close(pipefd[1]);
|
||||
|
||||
// Redirect stderr to /dev/null
|
||||
int devNull = open("/dev/null", O_WRONLY);
|
||||
if (devNull >= 0) {
|
||||
dup2(devNull, STDERR_FILENO);
|
||||
close(devNull);
|
||||
}
|
||||
|
||||
std::vector<char*> argv;
|
||||
for (auto& arg : args) {
|
||||
argv.push_back(const_cast<char*>(arg.c_str()));
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
|
||||
execv("/usr/bin/ffmpeg", argv.data());
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
// Parent process
|
||||
close(pipefd[1]); // Close write end
|
||||
|
||||
// Read raw samples (limit to reasonable amount - ~30 seconds of audio at 8kHz = 240KB)
|
||||
const size_t maxSamples = 8000 * 30; // 30 seconds worth
|
||||
std::vector<float> samples;
|
||||
samples.reserve(maxSamples);
|
||||
|
||||
float sample;
|
||||
ssize_t bytesRead;
|
||||
while (samples.size() < maxSamples &&
|
||||
(bytesRead = read(pipefd[0], &sample, sizeof(float))) == sizeof(float)) {
|
||||
samples.push_back(std::abs(sample)); // Store absolute value
|
||||
}
|
||||
close(pipefd[0]);
|
||||
|
||||
// Wait for child process
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
if (samples.empty()) {
|
||||
LOG_WARN << "No samples extracted for waveform generation";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Compute 200 peaks from samples
|
||||
const int numPeaks = 200;
|
||||
std::vector<float> peaks(numPeaks, 0.0f);
|
||||
size_t samplesPerPeak = samples.size() / numPeaks;
|
||||
if (samplesPerPeak == 0) samplesPerPeak = 1;
|
||||
|
||||
for (int i = 0; i < numPeaks; i++) {
|
||||
size_t start = i * samplesPerPeak;
|
||||
size_t end = std::min(start + samplesPerPeak, samples.size());
|
||||
float maxVal = 0.0f;
|
||||
for (size_t j = start; j < end; j++) {
|
||||
if (samples[j] > maxVal) maxVal = samples[j];
|
||||
}
|
||||
peaks[i] = maxVal;
|
||||
}
|
||||
|
||||
// Normalize peaks to 0-1 range
|
||||
float maxPeak = *std::max_element(peaks.begin(), peaks.end());
|
||||
if (maxPeak > 0.0f) {
|
||||
for (auto& p : peaks) {
|
||||
p /= maxPeak;
|
||||
}
|
||||
}
|
||||
|
||||
// Write JSON file
|
||||
try {
|
||||
std::ofstream ofs(waveformPath);
|
||||
if (!ofs) {
|
||||
LOG_ERROR << "Failed to create waveform file: " << waveformPath;
|
||||
return "";
|
||||
}
|
||||
|
||||
ofs << "{\"peaks\":[";
|
||||
for (size_t i = 0; i < peaks.size(); i++) {
|
||||
if (i > 0) ofs << ",";
|
||||
ofs << std::fixed << std::setprecision(3) << peaks[i];
|
||||
}
|
||||
ofs << "]}";
|
||||
ofs.close();
|
||||
|
||||
LOG_INFO << "Waveform generated for audio " << audioId << ": " << peaks.size() << " peaks";
|
||||
return "/uploads/audio/waveforms/" + std::to_string(audioId) + ".json";
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception writing waveform file: " << e.what();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Get audio duration in seconds using ffprobe (safe version)
|
||||
int getAudioDuration(const std::string& audioPath) {
|
||||
if (!isPathSafe(audioPath, "/app/uploads")) {
|
||||
|
|
@ -87,6 +228,7 @@ namespace {
|
|||
audio["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
audio["filePath"] = row["file_path"].as<std::string>();
|
||||
audio["thumbnailPath"] = row["thumbnail_path"].isNull() ? "" : row["thumbnail_path"].as<std::string>();
|
||||
audio["waveformPath"] = row["waveform_path"].isNull() ? "" : row["waveform_path"].as<std::string>();
|
||||
audio["durationSeconds"] = row["duration_seconds"].as<int>();
|
||||
audio["format"] = row["format"].isNull() ? "" : row["format"].as<std::string>();
|
||||
audio["bitrate"] = row["bitrate"].isNull() ? 0 : row["bitrate"].as<int>();
|
||||
|
|
@ -120,16 +262,32 @@ namespace {
|
|||
return;
|
||||
}
|
||||
|
||||
// Generate waveform data
|
||||
std::string waveformPath = generateWaveform(audioFullPath, audioId);
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "UPDATE audio_files SET duration_seconds = $1, bitrate = $2, format = $3, status = 'ready' WHERE id = $4"
|
||||
<< duration << bitrate << format << audioId
|
||||
>> [audioId](const Result&) {
|
||||
LOG_INFO << "Audio " << audioId << " metadata processed successfully";
|
||||
}
|
||||
>> [audioId](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to update audio " << audioId << " metadata: " << e.base().what();
|
||||
markAudioFailed(audioId);
|
||||
};
|
||||
if (!waveformPath.empty()) {
|
||||
*dbClient << "UPDATE audio_files SET duration_seconds = $1, bitrate = $2, format = $3, waveform_path = $4, status = 'ready' WHERE id = $5"
|
||||
<< duration << bitrate << format << waveformPath << audioId
|
||||
>> [audioId](const Result&) {
|
||||
LOG_INFO << "Audio " << audioId << " metadata and waveform processed successfully";
|
||||
}
|
||||
>> [audioId](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to update audio " << audioId << " metadata: " << e.base().what();
|
||||
markAudioFailed(audioId);
|
||||
};
|
||||
} else {
|
||||
// Still mark as ready even without waveform
|
||||
*dbClient << "UPDATE audio_files SET duration_seconds = $1, bitrate = $2, format = $3, status = 'ready' WHERE id = $4"
|
||||
<< duration << bitrate << format << audioId
|
||||
>> [audioId](const Result&) {
|
||||
LOG_INFO << "Audio " << audioId << " metadata processed (no waveform)";
|
||||
}
|
||||
>> [audioId](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to update audio " << audioId << " metadata: " << e.base().what();
|
||||
markAudioFailed(audioId);
|
||||
};
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception processing metadata for audio " << audioId << ": " << e.what();
|
||||
markAudioFailed(audioId);
|
||||
|
|
@ -143,7 +301,7 @@ void AudioController::getAllAudio(const HttpRequestPtr &req,
|
|||
auto pagination = parsePagination(req, 20, 50);
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, "
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, a.waveform_path, "
|
||||
"a.duration_seconds, a.format, a.bitrate, a.play_count, a.created_at, a.realm_id, "
|
||||
"u.id as user_id, u.username, u.avatar_url, "
|
||||
"r.name as realm_name "
|
||||
|
|
@ -170,7 +328,7 @@ void AudioController::getAllAudio(const HttpRequestPtr &req,
|
|||
void AudioController::getLatestAudio(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, "
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, a.waveform_path, "
|
||||
"a.duration_seconds, a.format, a.bitrate, a.play_count, a.created_at, a.realm_id, "
|
||||
"u.id as user_id, u.username, u.avatar_url, "
|
||||
"r.name as realm_name "
|
||||
|
|
@ -280,7 +438,7 @@ void AudioController::getRealmAudio(const HttpRequestPtr &req,
|
|||
return;
|
||||
}
|
||||
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, "
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, a.waveform_path, "
|
||||
"a.duration_seconds, a.format, a.bitrate, a.play_count, a.created_at "
|
||||
"FROM audio_files a "
|
||||
"WHERE a.realm_id = $1 AND a.is_public = true AND a.status = 'ready' "
|
||||
|
|
@ -339,7 +497,7 @@ void AudioController::getRealmAudioByName(const HttpRequestPtr &req,
|
|||
|
||||
int64_t realmId = realmResult[0]["id"].as<int64_t>();
|
||||
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, "
|
||||
*dbClient << "SELECT a.id, a.title, a.description, a.file_path, a.thumbnail_path, a.waveform_path, "
|
||||
"a.duration_seconds, a.format, a.bitrate, a.play_count, a.created_at "
|
||||
"FROM audio_files a "
|
||||
"WHERE a.realm_id = $1 AND a.is_public = true AND a.status = 'ready' "
|
||||
|
|
@ -422,6 +580,168 @@ void AudioController::incrementPlayCount(const HttpRequestPtr &req,
|
|||
});
|
||||
}
|
||||
|
||||
void AudioController::getWaveform(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &audioId) {
|
||||
int64_t id;
|
||||
try {
|
||||
id = std::stoll(audioId);
|
||||
} catch (...) {
|
||||
callback(jsonError("Invalid audio ID", k400BadRequest));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT waveform_path FROM audio_files WHERE id = $1 AND is_public = true AND status = 'ready'"
|
||||
<< id
|
||||
>> [callback, id](const Result& r) {
|
||||
if (r.empty()) {
|
||||
callback(jsonError("Audio not found", k404NotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string waveformPath = r[0]["waveform_path"].isNull() ? "" : r[0]["waveform_path"].as<std::string>();
|
||||
|
||||
if (waveformPath.empty()) {
|
||||
// Return empty peaks if no waveform available
|
||||
Json::Value resp;
|
||||
resp["peaks"] = Json::arrayValue;
|
||||
callback(jsonResp(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string fullPath = "/app" + waveformPath;
|
||||
|
||||
// Validate path is within allowed directory
|
||||
if (!isPathSafe(fullPath, "/app/uploads/audio/waveforms")) {
|
||||
LOG_WARN << "Blocked access to waveform file outside uploads: " << fullPath;
|
||||
Json::Value resp;
|
||||
resp["peaks"] = Json::arrayValue;
|
||||
callback(jsonResp(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and return the JSON file
|
||||
try {
|
||||
std::ifstream ifs(fullPath);
|
||||
if (!ifs) {
|
||||
Json::Value resp;
|
||||
resp["peaks"] = Json::arrayValue;
|
||||
callback(jsonResp(resp));
|
||||
return;
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << ifs.rdbuf();
|
||||
|
||||
Json::Value resp;
|
||||
Json::CharReaderBuilder builder;
|
||||
std::string errors;
|
||||
std::istringstream stream(buffer.str());
|
||||
if (Json::parseFromStream(builder, stream, &resp, &errors)) {
|
||||
// Add cache headers for waveforms (they don't change)
|
||||
auto response = jsonResp(resp);
|
||||
response->addHeader("Cache-Control", "public, max-age=31536000"); // 1 year
|
||||
callback(response);
|
||||
} else {
|
||||
Json::Value errResp;
|
||||
errResp["peaks"] = Json::arrayValue;
|
||||
callback(jsonResp(errResp));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Error reading waveform file: " << e.what();
|
||||
Json::Value resp;
|
||||
resp["peaks"] = Json::arrayValue;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
}
|
||||
>> DB_ERROR(callback, "get waveform");
|
||||
}
|
||||
|
||||
void AudioController::downloadAudio(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &audioId) {
|
||||
// Require authentication for downloads
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
if (user.id == 0) {
|
||||
callback(jsonError("Please log in to download audio", k401Unauthorized));
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t id;
|
||||
try {
|
||||
id = std::stoll(audioId);
|
||||
} catch (...) {
|
||||
callback(jsonError("Invalid audio ID", k400BadRequest));
|
||||
return;
|
||||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
|
||||
// Get audio info - only allow download of public, ready audio
|
||||
*dbClient << "SELECT title, file_path, format FROM audio_files WHERE id = $1 AND is_public = true AND status = 'ready'"
|
||||
<< id
|
||||
>> [callback, id](const Result& r) {
|
||||
if (r.empty()) {
|
||||
callback(jsonError("Audio not found", k404NotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string title = r[0]["title"].as<std::string>();
|
||||
std::string filePath = r[0]["file_path"].as<std::string>();
|
||||
std::string format = r[0]["format"].isNull() ? "mp3" : r[0]["format"].as<std::string>();
|
||||
std::string fullPath = "/app" + filePath;
|
||||
|
||||
// Validate path is within allowed directory
|
||||
if (!isPathSafe(fullPath, "/app/uploads/audio")) {
|
||||
LOG_WARN << "Blocked access to file outside uploads: " << fullPath;
|
||||
callback(jsonError("Audio file not found", k404NotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file exists
|
||||
if (!std::filesystem::exists(fullPath)) {
|
||||
LOG_ERROR << "Audio file not found: " << fullPath;
|
||||
callback(jsonError("Audio file not found", k404NotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitize title for filename (remove special chars)
|
||||
std::string safeTitle;
|
||||
for (char c : title) {
|
||||
if (std::isalnum(c) || c == ' ' || c == '-' || c == '_') {
|
||||
safeTitle += c;
|
||||
}
|
||||
}
|
||||
if (safeTitle.empty()) safeTitle = "audio";
|
||||
if (safeTitle.length() > 100) safeTitle = safeTitle.substr(0, 100);
|
||||
|
||||
// Determine content type based on format
|
||||
std::string contentType = "audio/mpeg"; // default for mp3
|
||||
std::string extension = "mp3";
|
||||
if (format == "wav") {
|
||||
contentType = "audio/wav";
|
||||
extension = "wav";
|
||||
} else if (format == "flac") {
|
||||
contentType = "audio/flac";
|
||||
extension = "flac";
|
||||
} else if (format == "ogg") {
|
||||
contentType = "audio/ogg";
|
||||
extension = "ogg";
|
||||
} else if (format == "aac" || format == "m4a") {
|
||||
contentType = "audio/mp4";
|
||||
extension = "m4a";
|
||||
}
|
||||
|
||||
// Use Drogon's file response for efficient streaming
|
||||
auto resp = HttpResponse::newFileResponse(fullPath, "", CT_CUSTOM);
|
||||
resp->addHeader("Content-Type", contentType);
|
||||
resp->addHeader("Content-Disposition", "attachment; filename=\"" + safeTitle + "." + extension + "\"");
|
||||
callback(resp);
|
||||
}
|
||||
>> DB_ERROR(callback, "download audio");
|
||||
}
|
||||
|
||||
void AudioController::getMyAudio(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public:
|
|||
ADD_METHOD_TO(AudioController::getRealmAudio, "/api/audio/realm/{1}", Get);
|
||||
ADD_METHOD_TO(AudioController::getRealmAudioByName, "/api/audio/realm/name/{1}", Get);
|
||||
ADD_METHOD_TO(AudioController::incrementPlayCount, "/api/audio/{1}/play", Post);
|
||||
ADD_METHOD_TO(AudioController::getWaveform, "/api/audio/{1}/waveform", Get);
|
||||
ADD_METHOD_TO(AudioController::downloadAudio, "/api/audio/{1}/download", Get);
|
||||
|
||||
// Authenticated endpoints
|
||||
ADD_METHOD_TO(AudioController::getMyAudio, "/api/user/audio", Get);
|
||||
|
|
@ -47,6 +49,14 @@ public:
|
|||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &audioId);
|
||||
|
||||
void getWaveform(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &audioId);
|
||||
|
||||
void downloadAudio(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &audioId);
|
||||
|
||||
// Authenticated audio management
|
||||
void getMyAudio(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
|
|
|||
|
|
@ -103,15 +103,15 @@ namespace {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper to create a new viewer token
|
||||
void createNewViewerToken(std::function<void(const HttpResponsePtr &)> callback, const std::string& streamKey) {
|
||||
// Helper to create a new viewer token (uses realm-specific cookies to support multi-stream viewing)
|
||||
void createNewViewerToken(std::function<void(const HttpResponsePtr &)> callback, const std::string& streamKey, const std::string& realmId) {
|
||||
auto bytes = drogon::utils::genRandomString(32);
|
||||
std::string token = drogon::utils::base64Encode(
|
||||
(const unsigned char*)bytes.data(), bytes.length()
|
||||
);
|
||||
|
||||
RedisHelper::storeKeyAsync("viewer_token:" + token, streamKey, 300,
|
||||
[callback, token](bool stored) {
|
||||
[callback, token, realmId](bool stored) {
|
||||
if (!stored) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setStatusCode(k500InternalServerError);
|
||||
|
|
@ -121,7 +121,8 @@ namespace {
|
|||
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
|
||||
Cookie cookie("viewer_token", token);
|
||||
// Use realm-specific cookie name to support multiple streams simultaneously
|
||||
Cookie cookie("viewer_token_" + realmId, token);
|
||||
cookie.setPath("/");
|
||||
cookie.setHttpOnly(true);
|
||||
cookie.setSecure(false);
|
||||
|
|
@ -393,13 +394,13 @@ void RealmController::issueRealmViewerToken(const HttpRequestPtr &req,
|
|||
const std::string &realmId) {
|
||||
int64_t id = std::stoll(realmId);
|
||||
|
||||
// Check for existing viewer token to avoid creating duplicates on page refresh
|
||||
auto existingToken = req->getCookie("viewer_token");
|
||||
// Check for existing viewer token for THIS specific realm (supports multi-stream viewing)
|
||||
auto existingToken = req->getCookie("viewer_token_" + realmId);
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true"
|
||||
<< id
|
||||
>> [callback, existingToken](const Result& r) {
|
||||
>> [callback, existingToken, realmId](const Result& r) {
|
||||
if (r.empty()) {
|
||||
callback(jsonResp({}, k404NotFound));
|
||||
return;
|
||||
|
|
@ -407,18 +408,18 @@ void RealmController::issueRealmViewerToken(const HttpRequestPtr &req,
|
|||
|
||||
std::string streamKey = r[0]["stream_key"].as<std::string>();
|
||||
|
||||
// If user has existing token, check if it's still valid for this stream
|
||||
// If user has existing token for this realm, check if it's still valid
|
||||
if (!existingToken.empty()) {
|
||||
RedisHelper::getKeyAsync("viewer_token:" + existingToken,
|
||||
[callback, existingToken, streamKey](const std::string& storedKey) {
|
||||
[callback, existingToken, streamKey, realmId](const std::string& storedKey) {
|
||||
if (storedKey == streamKey) {
|
||||
// Token is still valid for this stream - just refresh TTL and return it
|
||||
RedisHelper::storeKeyAsync("viewer_token:" + existingToken, streamKey, 300,
|
||||
[callback, existingToken](bool stored) {
|
||||
[callback, existingToken, realmId](bool stored) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
|
||||
// Refresh cookie
|
||||
Cookie cookie("viewer_token", existingToken);
|
||||
// Refresh realm-specific cookie
|
||||
Cookie cookie("viewer_token_" + realmId, existingToken);
|
||||
cookie.setPath("/");
|
||||
cookie.setHttpOnly(true);
|
||||
cookie.setSecure(false);
|
||||
|
|
@ -441,13 +442,13 @@ void RealmController::issueRealmViewerToken(const HttpRequestPtr &req,
|
|||
if (!storedKey.empty()) {
|
||||
RedisHelper::deleteKeyAsync("viewer_token:" + existingToken, [](bool){});
|
||||
}
|
||||
createNewViewerToken(callback, streamKey);
|
||||
createNewViewerToken(callback, streamKey, realmId);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// No existing token, create new one
|
||||
createNewViewerToken(callback, streamKey);
|
||||
// No existing token for this realm, create new one
|
||||
createNewViewerToken(callback, streamKey, realmId);
|
||||
}
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
|
|
@ -459,15 +460,15 @@ void RealmController::issueRealmViewerToken(const HttpRequestPtr &req,
|
|||
void RealmController::getRealmStreamKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId) {
|
||||
// Check for viewer token
|
||||
auto token = req->getCookie("viewer_token");
|
||||
// Check for realm-specific viewer token (supports multi-stream viewing)
|
||||
auto token = req->getCookie("viewer_token_" + realmId);
|
||||
if (token.empty()) {
|
||||
callback(jsonError("No viewer token", k403Forbidden));
|
||||
callback(jsonError("No viewer token for this realm", k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int64_t id = std::stoll(realmId);
|
||||
|
||||
|
||||
// First get the stream key for this realm
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true"
|
||||
|
|
@ -477,9 +478,9 @@ void RealmController::getRealmStreamKey(const HttpRequestPtr &req,
|
|||
callback(jsonError("Realm not found", k404NotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
std::string streamKey = r[0]["stream_key"].as<std::string>();
|
||||
|
||||
|
||||
// Verify the token is valid for this stream
|
||||
RedisHelper::getKeyAsync("viewer_token:" + token,
|
||||
[callback, streamKey](const std::string& storedStreamKey) {
|
||||
|
|
@ -487,7 +488,7 @@ void RealmController::getRealmStreamKey(const HttpRequestPtr &req,
|
|||
callback(jsonError("Invalid token for this realm", k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Token is valid, return the stream key
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
|
|
|
|||
|
|
@ -2212,44 +2212,50 @@ void UserController::getTreasury(const HttpRequestPtr &,
|
|||
double estimatedShare = totalUsers > 0 ? balance / static_cast<double>(totalUsers) : 0.0;
|
||||
estimatedShare = std::ceil(estimatedShare * 1000.0) / 1000.0;
|
||||
|
||||
// Calculate next Sunday (next distribution) in UTC
|
||||
// Calculate next Sunday (next distribution) in EST (UTC-5)
|
||||
// EST offset: -5 hours = -18000 seconds
|
||||
const int EST_OFFSET_SECONDS = -5 * 3600;
|
||||
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm nowUtc;
|
||||
std::time_t nowEst = now + EST_OFFSET_SECONDS;
|
||||
|
||||
std::tm nowEstTm;
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&nowUtc, &now);
|
||||
gmtime_s(&nowEstTm, &nowEst);
|
||||
#else
|
||||
gmtime_r(&now, &nowUtc);
|
||||
gmtime_r(&nowEst, &nowEstTm);
|
||||
#endif
|
||||
|
||||
// Days until Sunday (0 = Sunday)
|
||||
// If today is Sunday, next distribution is next Sunday (7 days)
|
||||
int daysUntilSunday = (7 - nowUtc.tm_wday) % 7;
|
||||
// Days until Sunday (0 = Sunday) based on EST
|
||||
int daysUntilSunday = (7 - nowEstTm.tm_wday) % 7;
|
||||
if (daysUntilSunday == 0) daysUntilSunday = 7;
|
||||
|
||||
// Calculate next Sunday at midnight UTC
|
||||
std::tm nextSundayTm = nowUtc;
|
||||
// Calculate next Sunday at midnight EST (which is 5 AM UTC)
|
||||
std::tm nextSundayTm = nowEstTm;
|
||||
nextSundayTm.tm_mday += daysUntilSunday;
|
||||
nextSundayTm.tm_hour = 0;
|
||||
nextSundayTm.tm_min = 0;
|
||||
nextSundayTm.tm_sec = 0;
|
||||
|
||||
// Normalize the tm struct (handles month overflow etc)
|
||||
// Note: timegm is the UTC version of mktime
|
||||
// Convert to time_t (treating as UTC since we applied offset)
|
||||
#ifdef _WIN32
|
||||
std::time_t nextSunday = _mkgmtime(&nextSundayTm);
|
||||
std::time_t nextSundayEst = _mkgmtime(&nextSundayTm);
|
||||
#else
|
||||
std::time_t nextSunday = timegm(&nextSundayTm);
|
||||
std::time_t nextSundayEst = timegm(&nextSundayTm);
|
||||
#endif
|
||||
|
||||
// Convert back to UTC for the response
|
||||
std::time_t nextSundayUtc = nextSundayEst - EST_OFFSET_SECONDS;
|
||||
|
||||
// Format as ISO 8601 UTC
|
||||
char nextDistBuffer[32];
|
||||
std::tm nextSundayUtc;
|
||||
std::tm nextSundayUtcTm;
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&nextSundayUtc, &nextSunday);
|
||||
gmtime_s(&nextSundayUtcTm, &nextSundayUtc);
|
||||
#else
|
||||
gmtime_r(&nextSunday, &nextSundayUtc);
|
||||
gmtime_r(&nextSundayUtc, &nextSundayUtcTm);
|
||||
#endif
|
||||
std::strftime(nextDistBuffer, sizeof(nextDistBuffer), "%Y-%m-%dT%H:%M:%SZ", &nextSundayUtc);
|
||||
std::strftime(nextDistBuffer, sizeof(nextDistBuffer), "%Y-%m-%dT%H:%M:%SZ", &nextSundayUtcTm);
|
||||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue