From d186e2b599856cc3fafba1fe19f087d83542472e Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Sat, 5 Nov 2022 13:02:30 +0100 Subject: [PATCH] add live status to playtwitch --- scripts/playtwitch/build.zig | 1 + scripts/playtwitch/src/State.zig | 12 +++++ scripts/playtwitch/src/config.zig | 12 +++-- scripts/playtwitch/src/ffi.zig | 2 + scripts/playtwitch/src/gui.zig | 27 +++++++++- scripts/playtwitch/src/live.zig | 85 +++++++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 scripts/playtwitch/src/live.zig diff --git a/scripts/playtwitch/build.zig b/scripts/playtwitch/build.zig index 6f5ea46..d062c68 100644 --- a/scripts/playtwitch/build.zig +++ b/scripts/playtwitch/build.zig @@ -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; diff --git a/scripts/playtwitch/src/State.zig b/scripts/playtwitch/src/State.zig index f279a8c..3dfeccb 100644 --- a/scripts/playtwitch/src/State.zig +++ b/scripts/playtwitch/src/State.zig @@ -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"); diff --git a/scripts/playtwitch/src/config.zig b/scripts/playtwitch/src/config.zig index 2fbf27b..53900ae 100644 --- a/scripts/playtwitch/src/config.zig +++ b/scripts/playtwitch/src/config.zig @@ -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); } diff --git a/scripts/playtwitch/src/ffi.zig b/scripts/playtwitch/src/ffi.zig index a91ce3c..fec9b2a 100644 --- a/scripts/playtwitch/src/ffi.zig +++ b/scripts/playtwitch/src/ffi.zig @@ -1,4 +1,6 @@ pub const c = @cImport({ + @cInclude("curl/curl.h"); + @cInclude("GL/glew.h"); @cInclude("GLFW/glfw3.h"); diff --git a/scripts/playtwitch/src/gui.zig b/scripts/playtwitch/src/gui.zig index d93da42..3cda213 100644 --- a/scripts/playtwitch/src/gui.zig +++ b/scripts/playtwitch/src/gui.zig @@ -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); } } diff --git a/scripts/playtwitch/src/live.zig b/scripts/playtwitch/src/live.zig new file mode 100644 index 0000000..c4cae8e --- /dev/null +++ b/scripts/playtwitch/src/live.zig @@ -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; + } +}