dotfiles/plugins/mzte-mpv/src/modules/SBSkip.zig

144 lines
4.6 KiB
Zig

const std = @import("std");
const c = ffi.c;
const ffi = @import("../ffi.zig");
const util = @import("../util.zig");
const ChapterSet = std.AutoHashMap(isize, void);
skipped_chapters: ChapterSet,
const SBSkip = @This();
const blacklist = std.ComptimeStringMap(void, .{
.{ "Endcards/Credits", {} },
.{ "Interaction Reminder", {} },
.{ "Intermission/Intro Animation", {} },
.{ "Intro", {} },
.{ "Outro", {} },
.{ "Sponsor", {} },
.{ "Unpaid/Self Promotion", {} },
});
pub fn onEvent(self: *SBSkip, mpv: *c.mpv_handle, ev: *c.mpv_event) !void {
switch (ev.event_id) {
c.MPV_EVENT_PROPERTY_CHANGE => {
const evprop: *c.mpv_event_property = @ptrCast(@alignCast(ev.data));
if (std.mem.eql(u8, std.mem.span(evprop.name), "chapter")) {
const chapter_id_ptr = @as(?*i64, @ptrCast(@alignCast(evprop.data)));
if (chapter_id_ptr) |chptr|
try self.onChapterChange(mpv, @intCast(chptr.*));
}
},
c.MPV_EVENT_FILE_LOADED => self.skipped_chapters.clearRetainingCapacity(),
else => {},
}
}
fn onChapterChange(
self: *SBSkip,
mpv: *c.mpv_handle,
chapter_id: isize,
) !void {
if (chapter_id < 0 or self.skipped_chapters.contains(chapter_id))
return;
// fuck these ubiquitous duck typing implementations everywhere! we have structs, for fuck's sake!
var chapter_list_node: c.mpv_node = undefined;
try ffi.checkMpvError(c.mpv_get_property(
mpv,
"chapter-list",
c.MPV_FORMAT_NODE,
&chapter_list_node,
));
defer c.mpv_free_node_contents(&chapter_list_node);
std.debug.assert(chapter_list_node.format == c.MPV_FORMAT_NODE_ARRAY);
const chapter_nodes = chapter_list_node.u.list.*.values[0..@intCast(chapter_list_node.u.list.*.num)];
std.debug.assert(chapter_nodes[@intCast(chapter_id)].format == c.MPV_FORMAT_NODE_MAP);
const chapter = Chapter.fromNodeMap(chapter_nodes[@intCast(chapter_id)].u.list.*);
if (chapter.skipReason()) |reason| {
const end_time = if (chapter_id != chapter_nodes.len - 1) end_time: {
std.debug.assert(chapter_nodes[@as(usize, @intCast(chapter_id)) + 1].format ==
c.MPV_FORMAT_NODE_MAP);
const next_chapter = Chapter.fromNodeMap(
chapter_nodes[@as(usize, @intCast(chapter_id)) + 1].u.list.*,
);
break :end_time next_chapter.time;
} else end_time: {
var end_time: f64 = 0.0;
try ffi.checkMpvError(c.mpv_get_property(
mpv,
"duration",
c.MPV_FORMAT_DOUBLE,
&end_time,
));
break :end_time end_time;
};
try ffi.checkMpvError(c.mpv_set_property(
mpv,
"time-pos",
c.MPV_FORMAT_DOUBLE,
@constCast(&end_time),
));
try self.skipped_chapters.put(chapter_id, {});
try util.msg(mpv, "skipped: {s}", .{reason});
}
}
const Chapter = struct {
title: [:0]const u8,
time: f64,
fn fromNodeMap(m: c.mpv_node_list) Chapter {
var self = Chapter{ .title = "", .time = 0 };
for (m.keys[0..@intCast(m.num)], m.values[0..@intCast(m.num)]) |k_c, v| {
const k = std.mem.span(k_c);
if (std.mem.eql(u8, k, "title")) {
std.debug.assert(v.format == c.MPV_FORMAT_STRING);
self.title = std.mem.span(v.u.string);
} else if (std.mem.eql(u8, k, "time")) {
std.debug.assert(v.format == c.MPV_FORMAT_DOUBLE);
self.time = v.u.double_;
}
}
return self;
}
/// Returns the reason for the chapter being skipped or null if the chapter should not be skipped.
fn skipReason(self: Chapter) ?[]const u8 {
const prefix = "[SponsorBlock]: ";
if (self.title.len <= prefix.len or !std.mem.startsWith(u8, self.title, prefix))
return null;
const types = self.title[prefix.len..];
var type_iter = std.mem.tokenize(u8, types, ",");
while (type_iter.next()) |type_split| {
const typestr = std.mem.trim(u8, type_split, &std.ascii.whitespace);
if (blacklist.has(typestr))
return typestr;
}
return null;
}
};
pub fn setup(self: *SBSkip, mpv: *c.mpv_handle) !void {
_ = self;
try ffi.checkMpvError(c.mpv_observe_property(mpv, 0, "chapter", c.MPV_FORMAT_INT64));
}
pub fn create() SBSkip {
return .{
.skipped_chapters = ChapterSet.init(std.heap.c_allocator),
};
}
pub fn deinit(self: *SBSkip) void {
self.skipped_chapters.deinit();
}