mirror of
https://mzte.de/git/LordMZTE/dotfiles.git
synced 2024-05-18 15:04:19 +02:00
rewrite mpv-sbskip in Zig as a C plugin
This commit is contained in:
parent
9c30757284
commit
34cdd162c3
|
@ -1,48 +0,0 @@
|
|||
;<! tmpl:setPostProcessor(opt.fennelCompile) !>
|
||||
; 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
|
6
plugins/README.md
Normal file
6
plugins/README.md
Normal file
|
@ -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.
|
1
plugins/mpv-sbskip/.gitignore
vendored
Normal file
1
plugins/mpv-sbskip/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/zig-*
|
23
plugins/mpv-sbskip/build.zig
Normal file
23
plugins/mpv-sbskip/build.zig
Normal file
|
@ -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);
|
||||
}
|
33
plugins/mpv-sbskip/src/ffi.zig
Normal file
33
plugins/mpv-sbskip/src/ffi.zig
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
166
plugins/mpv-sbskip/src/main.zig
Normal file
166
plugins/mpv-sbskip/src/main.zig
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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)]
|
||||
|
|
6
setup/commands/install-plugins.rkt
Normal file
6
setup/commands/install-plugins.rkt
Normal file
|
@ -0,0 +1,6 @@
|
|||
#lang racket
|
||||
(require "../common.rkt")
|
||||
(provide run)
|
||||
|
||||
(define (run)
|
||||
(install-zig "plugins/mpv-sbskip"))
|
Loading…
Reference in a new issue