diff --git a/.config/mpv/scripts/sbskip.lua.cgt b/.config/mpv/scripts/sbskip.lua.cgt deleted file mode 100644 index 6626dc3..0000000 --- a/.config/mpv/scripts/sbskip.lua.cgt +++ /dev/null @@ -1,48 +0,0 @@ -; -; vim: filetype=fennel - -;; MPV script to skip SponsorBlock segments added by yt-dlp's `--sponsorblock-mark=all` option - -;; list of SponsorBlock segment types to skip -(local blacklist [:Intro - :Sponsor - :Outro - :Endcards/Credits - "Intermission/Intro Animation" - "Interaction Reminder" - "Unpaid/Self Promotion"]) - -;; chapters alredy skipped this file -;; table of chapter id => true -(var skipped {}) - -(fn should-skip [typestr] - (accumulate [matched false - ty (string.gmatch typestr "([^,]+),?%s*") - &until matched] - (accumulate [blacklisted false - _ bl (ipairs blacklist) - &until blacklisted] - (= ty bl)))) - -(fn msg [msg] - (print msg) - (mp.osd_message (.. "[sbskip] " msg) 4)) - -(fn on-chapter-change [_ chapter#] - (when (and chapter# (not (. skipped chapter#))) - (let [chapter-list (mp.get_property_native :chapter-list) - chapter (. chapter-list (+ chapter# 1)) - next (. chapter-list (+ chapter# 2)) - seg-type (string.match chapter.title "%[SponsorBlock%]: (.*)")] - ;; when the pattern matches and the type isn't blacklisted... - (when (and seg-type (should-skip seg-type)) - (msg (.. "skip: " seg-type)) - ;; add to skipped to not skip chapter again - (tset skipped chapter# true) - ;; set time to start of next chapter or end of video - (mp.set_property :time-pos (if next next.time - (mp.get_property_native :duration))))))) - -(mp.observe_property :chapter :number on-chapter-change) -(mp.register_event :file-loaded #(set skipped {})) ;; reset skipped chapters on file load diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..9b01e6e --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,6 @@ +# plugins +This directory contains a set of plugins for various software. + +## list + +- `mpv-sbskip`: a MPV plugin written in Zig to skip SponsorBlock segments marked by yt-dlp. diff --git a/plugins/mpv-sbskip/.gitignore b/plugins/mpv-sbskip/.gitignore new file mode 100644 index 0000000..fe95f8d --- /dev/null +++ b/plugins/mpv-sbskip/.gitignore @@ -0,0 +1 @@ +/zig-* diff --git a/plugins/mpv-sbskip/build.zig b/plugins/mpv-sbskip/build.zig new file mode 100644 index 0000000..5b40a83 --- /dev/null +++ b/plugins/mpv-sbskip/build.zig @@ -0,0 +1,23 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addSharedLibrary(.{ + .name = "mpv-sbskip", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + lib.linkLibC(); + + const install_step = b.addInstallArtifact(lib, .{ + // this is not a standard MPV installation path, but instead one that makes sense. + // this requires a symlink ../../../.local/share/mpv/scripts => ~/.config/mpv/scripts + .dest_dir = .{ .override = .{ .custom = "share/mpv/scripts" } }, + .dest_sub_path = "sbskip.so", + }); + b.getInstallStep().dependOn(&install_step.step); +} diff --git a/plugins/mpv-sbskip/src/ffi.zig b/plugins/mpv-sbskip/src/ffi.zig new file mode 100644 index 0000000..7bf25e3 --- /dev/null +++ b/plugins/mpv-sbskip/src/ffi.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +pub const c = @cImport({ + @cInclude("mpv/client.h"); +}); + +pub fn checkMpvError(err: c_int) !void { + if (err >= 0) + return; + + return switch (err) { + c.MPV_ERROR_EVENT_QUEUE_FULL => error.EventQueueFull, + c.MPV_ERROR_NOMEM => error.OutOfMemory, + c.MPV_ERROR_UNINITIALIZED => error.Uninitialized, + c.MPV_ERROR_INVALID_PARAMETER => error.InvalidParameter, + c.MPV_ERROR_OPTION_NOT_FOUND => error.OptionNotFound, + c.MPV_ERROR_OPTION_FORMAT => error.OptionFormat, + c.MPV_ERROR_OPTION_ERROR => error.OptionError, + c.MPV_ERROR_PROPERTY_NOT_FOUND => error.PropertyNotFound, + c.MPV_ERROR_PROPERTY_FORMAT => error.PropertyFormat, + c.MPV_ERROR_PROPERTY_UNAVAILABLE => error.PropertyUnavailable, + c.MPV_ERROR_PROPERTY_ERROR => error.PropertyError, + c.MPV_ERROR_COMMAND => error.Command, + c.MPV_ERROR_LOADING_FAILED => error.LoadingFailed, + c.MPV_ERROR_AO_INIT_FAILED => error.AOInitFailed, + c.MPV_ERROR_VO_INIT_FAILED => error.VOInitFailed, + c.MPV_ERROR_NOTHING_TO_PLAY => error.NothingToPlay, + c.MPV_ERROR_UNKNOWN_FORMAT => error.UnknownFormat, + c.MPV_ERROR_UNSUPPORTED => error.Unsupported, + c.MPV_ERROR_NOT_IMPLEMENTED => error.NotImplemented, + c.MPV_ERROR_GENERIC => error.Generic, + else => error.Unknown, + }; +} diff --git a/plugins/mpv-sbskip/src/main.zig b/plugins/mpv-sbskip/src/main.zig new file mode 100644 index 0000000..957c324 --- /dev/null +++ b/plugins/mpv-sbskip/src/main.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const ffi = @import("ffi.zig"); +const c = ffi.c; + +const blacklist = std.ComptimeStringMap(void, .{ + .{ "Endcards/Credits", {} }, + .{ "Interaction Reminder", {} }, + .{ "Intermission/Intro Animation", {} }, + .{ "Intro", {} }, + .{ "Outro", {} }, + .{ "Sponsor", {} }, + .{ "Unpaid/Self Promotion", {} }, +}); + +pub const std_options = struct { + pub const log_level = .debug; + pub fn logFn( + comptime message_level: std.log.Level, + comptime scope: @TypeOf(.enum_literal), + comptime format: []const u8, + args: anytype, + ) void { + _ = scope; + + const stderr = std.io.getStdErr().writer(); + + stderr.print("[sbskip {s}] " ++ format ++ "\n", .{@tagName(message_level)} ++ args) catch return; + } +}; + +export fn mpv_open_cplugin(handle: *c.mpv_handle) callconv(.C) c_int { + tryMain(handle) catch |e| { + if (@errorReturnTrace()) |ert| + std.debug.dumpStackTrace(ert.*); + std.log.err("{}", .{e}); + return -1; + }; + return 0; +} + +fn tryMain(mpv: *c.mpv_handle) !void { + var skipped_chapter_ids = std.AutoHashMap(usize, void).init(std.heap.c_allocator); + defer skipped_chapter_ids.deinit(); + + try ffi.checkMpvError(c.mpv_observe_property(mpv, 0, "chapter", c.MPV_FORMAT_INT64)); + + std.log.info("loaded with client name '{s}'", .{c.mpv_client_name(mpv)}); + + while (true) { + const ev = @as(*c.mpv_event, c.mpv_wait_event(mpv, -1)); + try ffi.checkMpvError(ev.@"error"); + switch (ev.event_id) { + c.MPV_EVENT_PROPERTY_CHANGE => { + const evprop: *c.mpv_event_property = @ptrCast(@alignCast(ev.data)); + if (std.mem.eql(u8, "chapter", std.mem.span(evprop.name))) { + const chapter_id_ptr = @as(?*i64, @ptrCast(@alignCast(evprop.data))); + if (chapter_id_ptr) |chptr| + try onChapterChange(mpv, @intCast(chptr.*), &skipped_chapter_ids); + } + }, + c.MPV_EVENT_FILE_LOADED => skipped_chapter_ids.clearRetainingCapacity(), + c.MPV_EVENT_SHUTDOWN => break, + else => {}, + } + } +} + +fn msg(mpv: *c.mpv_handle, comptime fmt: []const u8, args: anytype) !void { + std.log.info(fmt, args); + + var buf: [1024 * 4]u8 = undefined; + const osd_msg = try std.fmt.bufPrintZ(&buf, "[sbskip] " ++ fmt, args); + try ffi.checkMpvError(c.mpv_command( + mpv, + @constCast(&[_:null]?[*:0]const u8{ "show-text", osd_msg, "4000" }), + )); +} + +fn onChapterChange( + mpv: *c.mpv_handle, + chapter_id: usize, + skipped: *std.AutoHashMap(usize, void), +) !void { + if (skipped.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[chapter_id].format == c.MPV_FORMAT_NODE_MAP); + const chapter = Chapter.fromNodeMap(chapter_nodes[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[chapter_id + 1].format == c.MPV_FORMAT_NODE_MAP); + const next_chapter = Chapter.fromNodeMap(chapter_nodes[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 skipped.put(chapter_id, {}); + try 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; + } +}; diff --git a/setup.rkt b/setup.rkt index 30c0400..f72a220 100755 --- a/setup.rkt +++ b/setup.rkt @@ -7,13 +7,13 @@ "setup/common.rkt") ;; Valid verbs -(define verbs '(install-scripts install-lsps-paru setup-nvim-config confgen)) +(define verbs '(install-scripts install-plugins install-lsps-paru setup-nvim-config confgen)) (define verb (command-line #:program "setup.rkt" #:usage-help "Sets up my dotfiles. Available verbs:" - "install-scripts, install-lsps-paru, setup-nvim-config, confgen" + "install-scripts, install-plugins, install-lsps-paru, setup-nvim-config, confgen" #:once-each [("-o" "--bin-output") o "Output directory for executables" (output-bin-path o)] #:args (verb) (string->symbol verb))) @@ -35,6 +35,9 @@ ['install-scripts (local-require "setup/commands/install-scripts.rkt") (run)] + ['install-plugins + (local-require "setup/commands/install-plugins.rkt") + (run)] ['install-lsps-paru (local-require "setup/commands/install-lsps-paru.rkt") (run)] diff --git a/setup/commands/install-plugins.rkt b/setup/commands/install-plugins.rkt new file mode 100644 index 0000000..923989e --- /dev/null +++ b/setup/commands/install-plugins.rkt @@ -0,0 +1,6 @@ +#lang racket +(require "../common.rkt") +(provide run) + +(define (run) + (install-zig "plugins/mpv-sbskip"))