feat: receive ordered stream playback queues

This commit is contained in:
2026-10-22 14:06:47 +02:00
parent e7a33ff68e
commit 70672321a1
9 changed files with 239 additions and 7 deletions
+80 -1
View File
@@ -28,7 +28,7 @@ void appendNumber(std::string &record, double value) {
}
ProgressReporter::ProgressReporter(const std::string &path, std::function<double()> clock)
: clock(clock ? std::move(clock) : monotonicSeconds) {
: path(path), clock(clock ? std::move(clock) : monotonicSeconds) {
if (path.empty() || path.find('\0') != std::string::npos)
throw std::invalid_argument("The progress file path must be nonempty and contain no NUL bytes");
descriptor = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
@@ -49,6 +49,8 @@ ProgressReporter::ProgressReporter(const std::string &path, std::function<double
throw std::runtime_error(std::string("Could not verify progress file permissions: ") + std::strerror(errno));
if ((info.st_mode & 07777) != 0600)
throw std::runtime_error("The progress file must have exactly owner read/write permissions (0600)");
device = info.st_dev;
inode = info.st_ino;
writer = std::thread([this] { writeRecords(); });
} catch (...) {
::close(descriptor);
@@ -59,6 +61,31 @@ ProgressReporter::ProgressReporter(const std::string &path, std::function<double
ProgressReporter::~ProgressReporter() { finish(); }
void ProgressReporter::resume() {
if (!finished) return;
if (disabled.load(std::memory_order_acquire))
throw std::runtime_error("Cannot resume a failed progress report");
descriptor = ::open(path.c_str(), O_WRONLY | O_APPEND | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC);
if (descriptor < 0) throw std::runtime_error("Could not reopen the owned progress report");
struct stat info{};
if (::fstat(descriptor, &info) != 0 || !S_ISREG(info.st_mode) ||
info.st_dev != device || info.st_ino != inode || info.st_uid != ::geteuid() ||
info.st_nlink != 1 || (info.st_mode & 07777) != 0600) {
::close(descriptor);
descriptor = -1;
throw std::runtime_error("The owned progress report was replaced or is no longer private");
}
try {
stopping = false;
writer = std::thread([this] { writeRecords(); });
finished = false;
} catch (...) {
::close(descriptor);
descriptor = -1;
throw;
}
}
bool ProgressReporter::validMediaURL(std::string_view url) noexcept {
constexpr std::string_view prefix = "https://www.youtube.com/watch?v=";
if (url.size() != prefix.size() + 11 || url.substr(0, prefix.size()) != prefix) return false;
@@ -261,4 +288,56 @@ void ProgressReporter::finish() noexcept {
wake.notify_all();
if (writer.joinable()) writer.join();
}
ProgressReports::ProgressReports(const std::string &path)
: single(std::make_unique<ProgressReporter>(path)), active(single.get()) {}
void ProgressReports::registerFile(const std::string &identity, const std::string &path) {
if (single || !ProgressReporter::validMediaIdentity(identity) || entries.size() >= 4096 ||
path.empty() || path.front() != '/' || path.find('\0') != std::string::npos ||
entries.contains(identity))
throw std::runtime_error("Invalid or duplicate queue progress registration");
for (const auto &[key, entry] : entries)
if (entry.path == path) throw std::runtime_error("Duplicate queue progress report path");
entries.emplace(identity, Entry{path, nullptr});
}
void ProgressReports::begin(const std::string &identity, bool live) noexcept {
std::lock_guard lock(mutex);
if (single) { active->begin(identity, live); return; }
const auto found = entries.find(identity);
auto *next = found == entries.end() ? nullptr : found->second.reporter.get();
if (active != next && active) active->finish();
active = nullptr; // A failed or unregistered identity must never reuse the old report.
if (found == entries.end()) return;
try {
auto &entry = found->second;
if (!entry.reporter) entry.reporter = std::make_unique<ProgressReporter>(entry.path);
else entry.reporter->resume();
active = entry.reporter.get();
active->begin(identity, live);
} catch (const std::exception &exception) {
try { if (failure.empty()) failure = exception.what(); } catch (...) {}
} catch (...) {
try { if (failure.empty()) failure = "Could not select queue progress report"; } catch (...) {}
}
}
void ProgressReports::finish() noexcept {
std::lock_guard lock(mutex);
if (active) active->finish();
}
std::string ProgressReports::error() const {
std::lock_guard lock(mutex);
if (!failure.empty()) return failure;
if (single) return single->error();
for (const auto &[identity, entry] : entries) {
if (entry.reporter) {
auto message = entry.reporter->error();
if (!message.empty()) return message;
}
}
return {};
}
}