add live status to playtwitch

This commit is contained in:
LordMZTE 2022-11-05 13:02:30 +01:00
parent d9dfa0587d
commit d186e2b599
Signed by: LordMZTE
GPG key ID: B64802DC33A64FF6
6 changed files with 134 additions and 5 deletions

View file

@ -19,6 +19,7 @@ pub fn build(b: *std.build.Builder) void {
exe.linkSystemLibrary("cimgui");
exe.linkSystemLibrary("glfw3");
exe.linkSystemLibrary("glew");
exe.linkSystemLibrary("curl");
exe.strip = mode != .Debug;

View file

@ -6,6 +6,13 @@ const log = std.log.scoped(.state);
pub const ChannelEntry = struct {
name: []const u8,
comment: ?[]const u8,
live: Live = .loading,
};
pub const Live = enum {
live,
offline,
loading,
};
mutex: std.Thread.Mutex,
@ -27,6 +34,9 @@ channel_name_buf: [64]u8,
streamlink_memfd: ?std.fs.File,
streamlink_out: ?[]align(std.mem.page_size) u8,
/// If the status of the channels is being loaded currently
live_status_loading: bool,
const Self = @This();
pub fn init(win: *c.GLFWwindow) !*Self {
@ -50,6 +60,8 @@ pub fn init(win: *c.GLFWwindow) !*Self {
.streamlink_memfd = null,
.streamlink_out = null,
.live_status_loading = true,
};
std.mem.copy(u8, &self.quality_buf, "best");

View file

@ -66,9 +66,13 @@ pub fn configLoaderThread(state: *State) !void {
.{ channels.items.len, end_time - start_time },
);
state.mutex.lock();
defer state.mutex.unlock();
{
state.mutex.lock();
defer state.mutex.unlock();
state.channels_file_data = channels_data;
state.channels = channels.toOwnedSlice();
state.channels_file_data = channels_data;
state.channels = channels.toOwnedSlice();
}
try @import("live.zig").fetchChannelsLive(state);
}

View file

@ -1,4 +1,6 @@
pub const c = @cImport({
@cInclude("curl/curl.h");
@cInclude("GL/glew.h");
@cInclude("GLFW/glfw3.h");

View file

@ -97,6 +97,15 @@ pub fn winContent(state: *State) !void {
start = .channel_bar;
}
if (state.channels != null) {
c.igBeginDisabled(state.live_status_loading);
defer c.igEndDisabled();
if (c.igButton("Refresh Status", .{ .x = 0.0, .y = 0.0 })) {
(try std.Thread.spawn(.{}, @import("live.zig").reloadLiveThread, .{state}))
.detach();
}
}
if (state.channels != null and c.igBeginChild_Str(
"Quick Pick",
.{ .x = 0.0, .y = 0.0 },
@ -105,18 +114,24 @@ pub fn winContent(state: *State) !void {
)) {
_ = c.igBeginTable(
"##qp_table",
2,
3,
c.ImGuiTableFlags_Resizable,
.{ .x = 0.0, .y = 0.0 },
0.0,
);
defer c.igEndTable();
c.igTableSetupColumn("Channel", 0, 0.0, 0);
c.igTableSetupColumn("Comment", 0, 0.0, 0);
c.igTableSetupColumn("Live?", c.ImGuiTableColumnFlags_WidthFixed, 80.0, 0);
c.igTableHeadersRow();
_ = c.igTableSetColumnIndex(0);
c.igTableHeader("Channel");
_ = c.igTableSetColumnIndex(1);
c.igTableHeader("Comment");
_ = c.igTableSetColumnIndex(2);
c.igTableHeader("Live?");
for (state.channels.?) |ch, i| {
var ch_buf: [256]u8 = undefined;
@ -146,6 +161,16 @@ pub fn winContent(state: *State) !void {
if (ch.comment) |comment| {
igu.sliceText(comment);
}
_ = c.igTableSetColumnIndex(2);
const live_label = switch (ch.live) {
.loading => "Loading...",
.live => "Live",
.offline => "Offline",
};
igu.sliceText(live_label);
}
}

View file

@ -0,0 +1,85 @@
const std = @import("std");
const State = @import("State.zig");
const c = @import("ffi.zig").c;
const log = std.log.scoped(.live);
pub fn reloadLiveThread(s: *State) !void {
{
s.mutex.lock();
defer s.mutex.unlock();
for (s.channels.?) |chan| {
chan.live = .loading;
}
}
try fetchChannelsLive(s);
}
pub fn fetchChannelsLive(s: *State) !void {
@atomicStore(bool, &s.live_status_loading, true, .Unordered);
defer @atomicStore(bool, &s.live_status_loading, false, .Unordered);
log.info("initiaizing cURL", .{});
var curl = c.curl_easy_init();
if (curl == null)
return error.CurlInitError;
defer c.curl_easy_cleanup(curl);
try handleCurlErr(c.curl_easy_setopt(
curl,
c.CURLOPT_WRITEFUNCTION,
&curlWriteCb,
));
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_NOPROGRESS, @as(c_long, 1)));
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_FOLLOWLOCATION, @as(c_long, 1)));
// the twitch info grabbinator works by downloading the web page
// and checking if it contains a string. this is the bufffer for the page.
//
// Fuck you, twitch! amazing API design!
var page_buf = std.ArrayList(u8).init(std.heap.c_allocator);
defer page_buf.deinit();
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_WRITEDATA, &page_buf));
// we shouldn't need to aquire the mutex here, this data isnt being read and we're
// only doing atomic writes.
var fmt_buf: [512]u8 = undefined;
for (s.channels.?) |chan| {
page_buf.clearRetainingCapacity();
log.info("requesting live state for channel {s}", .{chan.name});
const url = try std.fmt.bufPrintZ(
&fmt_buf,
"https://www.twitch.tv/{s}",
.{chan.name},
);
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_URL, url.ptr));
try handleCurlErr(c.curl_easy_perform(curl));
if (std.mem.containsAtLeast(u8, page_buf.items, 1, "live_user")) {
@atomicStore(State.Live, &chan.live, .live, .Unordered);
} else {
@atomicStore(State.Live, &chan.live, .offline, .Unordered);
}
}
}
fn curlWriteCb(
data: [*]const u8,
size: usize,
nmemb: usize,
out: *std.ArrayList(u8),
) callconv(.C) usize {
const realsize = size * nmemb;
out.writer().writeAll(data[0..realsize]) catch return 0;
return realsize;
}
fn handleCurlErr(code: c.CURLcode) !void {
if (code != c.CURLE_OK) {
log.err("Curl error: {s}", .{c.curl_easy_strerror(code)});
return error.CurlError;
}
}