mpv/scripts/animecards.lua

497 lines
14 KiB
Lua
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

------------- Instructions -------------
-- -- Video Demonstration: https://www.youtube.com/watch?v=M4t7HYS73ZQ
-- IF USING WEBSOCKET (RECOMMENDED)
-- -- Install the mpv_webscoket extension: https://github.com/kuroahna/mpv_websocket
-- -- Open a LOCAL copy of https://github.com/Renji-XD/texthooker-ui
-- -- Configure the script (if you're not using the Lapis note format)
-- IF USING CLIPBOARD INSERTER (NOT RECOMMENDED)
-- -- Install the clipboard inserter plugin: https://github.com/laplus-sadness/lap-clipboard-inserter
-- -- Open the texthooker UI, enable the plugin and enable clipboard pasting: https://github.com/Renji-XD/texthooker-ui
-- BOTH
-- -- Wait for an unknown word and create the card with Yomichan.
-- -- Select all the subtitle lines you wish to add to the card and copy with Ctrl + c.
-- -- Press Ctrl + v in MPV to add the lines, their Audio and the currently paused image to the back of the card.
---------------------------------------
------------- Credits -------------
-- Credits and copyright go to Anacreon DJT: https://anacreondjt.gitlab.io/
------------------------------------
------------- Original Credits (Outdated) -------------
-- This script was made by users of 4chan's Daily Japanese Thread (DJT) on /jp/
-- More information can be found here http://animecards.site/
-- Message @Anacreon with bug reports and feature requests on Discord (https://animecards.site/discord/) or 4chan (https://boards.4channel.org/jp/#s=djt)
--
-- If you like this work please consider subscribing on Patreon!
-- https://www.patreon.com/Quizmaster
------------------------------------
local utils = require("mp.utils")
local msg = require("mp.msg")
------------- User Config -------------
-- Set these to match your field names in Anki
local FRONT_FIELD = "Expression"
local SENTENCE_AUDIO_FIELD = "SentenceAudio"
local SENTENCE_FIELD = "Sentence"
local IMAGE_FIELD = "Picture"
-- Optional padding and fade settings in seconds.
-- Padding grabs extra audio around your selected subs.
-- Fade does a volume fade effect at the beginning and end of the resulting audio.
local AUDIO_CLIP_FADE = 0.2
local AUDIO_CLIP_PADDING = 0.75
-- Optional play sentence audio automatically after card update
local AUTOPLAY_AUDIO = false
-- Optional screenshot image format. Valid options: "webp" or "png"
-- Change to "png" if you plan to view cards on iOS or Mac.
local IMAGE_FORMAT = "png"
-- Optional set to true if you want your volume in mpv to affect Anki card volume.
local USE_MPV_VOLUME = false
-- Set to true if you want writing to clipboard to be enabled by default.
-- The more modern and recommended alternative is to use the websocket.
local ENABLE_SUBS_TO_CLIP = false
---------------------------------------
------------- Internal Variables -------------
local subs = {}
local debug_mode = false
local use_powershell_clipboard = nil
local prefix = ""
---------------------------------------
------------- Setup -------------
if unpack ~= nil then
table.unpack = unpack
end
local o = {}
-- Possible platforms: windows, linux, macos
local platform = mp.get_property_native("platform")
if platform == "darwin" then
platform = "macos"
end
local display_server
if os.getenv("WAYLAND_DISPLAY") then
display_server = "wayland"
elseif platform == "linux" then
display_server = "xorg"
else
display_server = ""
end
local function dlog(...)
if debug_mode then
print(...)
end
end
local function verfiy_libmp3lame()
local encoderlist = mp.get_property("encoder-list")
if not encoderlist or not string.find(encoderlist, "libmp3lame") then
mp.osd_message(
"Error: libmp3lame encoder not found. Audio export will not work.\nPlease use a build of mpv with libmp3lame support.",
10
)
msg.error("Error: libmp3lame encoder not found. MP3 audio export will not work.")
else
dlog("libmp3lame encoder found.")
end
end
mp.register_event("file-loaded", verfiy_libmp3lame)
dlog("Detected Platform: " .. platform)
dlog("Detected display server: " .. display_server)
---------------------------------------
-- Handle requests to AnkiConnect
local function anki_connect(action, params)
local request = utils.format_json({ action = action, params = params, version = 6 })
local args = { "curl", "-s", "localhost:8765", "-X", "POST", "-d", request }
dlog("AnkiConnect request: " .. request)
local result = utils.subprocess({ args = args, cancellable = false, capture_stderr = true })
if result.status ~= 0 then
msg.error("Curl command failed with status: " .. tostring(result.status))
msg.error("Stderr: " .. (result.stderr or "none"))
return nil
end
if not result.stdout or result.stdout == "" then
msg.error("Empty response from AnkiConnect")
return nil
end
dlog("AnkiConnect response: " .. result.stdout)
local success, parsed_result = pcall(function()
return utils.parse_json(result.stdout)
end)
if not success or not parsed_result then
msg.error("Failed to parse JSON response: " .. (result.stdout or "empty"))
return nil
end
return parsed_result
end
-- Get media directory path from AnkiConnect
local function set_media_dir()
local media_dir_response = anki_connect("getMediaDirPath")
if not media_dir_response then
msg.error("Failed to communicate with AnkiConnect. Is Anki running and do you have AnkiConnect installed?")
mp.osd_message(
"Error: Failed to communicate with AnkiConnect. Is Anki running and do you have AnkiConnect installed?",
5
)
return
elseif media_dir_response["error"] then
msg.error("AnkiConnect error: " .. tostring(media_dir_response["error"]))
mp.osd_message("AnkiConnect error: " .. tostring(media_dir_response["error"]), 5)
return
elseif media_dir_response["result"] then
prefix = media_dir_response["result"]
dlog("Got media directory path from AnkiConnect: " .. prefix)
else
msg.error("Unexpected response format from AnkiConnect")
mp.osd_message("Error: Unexpected response from AnkiConnect", 5)
return
end
end
local function clean(s)
for _, ws in ipairs({
"%s",
" ",
"",
" ",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
" ",
"",
"",
}) do
s = s:gsub(ws .. "+", "")
end
return s
end
local function get_name(s, e)
return mp.get_property("filename"):gsub("%W", "") .. tostring(s) .. tostring(e)
end
local function get_clipboard()
local res
if platform == "windows" then
res = utils.subprocess({
args = {
"powershell",
"-NoProfile",
"-Command",
[[& {
Trap {
Write-Error -ErrorRecord $_
Exit 1
}
$clip = ""
if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
} else {
Add-Type -AssemblyName PresentationCore
$clip = [Windows.Clipboard]::GetText()
}
$clip = $clip -Replace "`r",""
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
}]],
},
})
elseif platform == "macos" then
return io.popen("LANG=en_US.UTF-8 pbpaste"):read("*a")
else -- platform == 'linux'
if display_server == "wayland" then
res = utils.subprocess({ args = {
"wl-paste",
} })
else -- display_server == 'xorg'
res = utils.subprocess({ args = {
"xclip",
"-selection",
"clipboard",
"-out",
} })
end
end
if not res.error then
return res.stdout
end
end
local function powershell_set_clipboard(text)
utils.subprocess({
args = {
"powershell",
"-NoProfile",
"-Command",
[[Set-Clipboard -Value @"]] .. "\n" .. text .. "\n" .. [["@]],
},
})
end
local function cmd_set_clipboard(text)
local cmd = "echo " .. text .. " | clip"
mp.command("run cmd /D /C " .. cmd)
end
local function determine_clip_type()
powershell_set_clipboard([[Anacreon様]])
use_powershell_clipboard = get_clipboard() == [[Anacreon様]]
end
local function linux_set_clipboard(text)
if display_server == "wayland" then
os.execute("wl-copy <<EOF\n" .. text .. "\nEOF\n")
else -- display_server == 'xorg'
os.execute("xclip -selection clipboard <<EOF\n" .. text .. "\nEOF\n")
end
end
local function macos_set_clipboard(text)
os.execute("export LANG=en_US.UTF-8; cat <<EOF | pbcopy\n" .. text .. "\nEOF\n")
end
local function record_sub(_, text)
if text and mp.get_property_number("sub-start") and mp.get_property_number("sub-end") then
local sub_delay = mp.get_property_native("sub-delay")
local audio_delay = mp.get_property_native("audio-delay")
local newtext = clean(text)
if newtext == "" then
return
end
subs[newtext] = {
mp.get_property_number("sub-start") + sub_delay - audio_delay,
mp.get_property_number("sub-end") + sub_delay - audio_delay,
}
dlog(string.format("%s -> %s : %s", subs[newtext][1], subs[newtext][2], newtext))
if ENABLE_SUBS_TO_CLIP then
-- Remove newlines from text before sending it to clipboard.
-- This way pressing control+v without copying from texthooker page
-- will always give last line.
text = string.gsub(text, "[\n\r]+", " ")
if platform == "windows" then
if use_powershell_clipboard == nil then
determine_clip_type()
end
if use_powershell_clipboard then
powershell_set_clipboard(text)
else
cmd_set_clipboard(text)
end
elseif platform == "macos" then
macos_set_clipboard(text)
else
linux_set_clipboard(text)
end
end
end
end
local function create_audio(s, e)
if s == nil or e == nil then
return
end
local name = get_name(s, e)
local destination = utils.join_path(prefix, name .. ".mp3")
s = s - AUDIO_CLIP_PADDING
local t = e - s + AUDIO_CLIP_PADDING
local source = mp.get_property("path")
local aid = mp.get_property("aid")
local tracks_count = mp.get_property_number("track-list/count")
for i = 1, tracks_count do
local track_type = mp.get_property(string.format("track-list/%d/type", i))
local track_selected = mp.get_property(string.format("track-list/%d/selected", i))
if track_type == "audio" and track_selected == "yes" then
if mp.get_property(string.format("track-list/%d/external-filename", i), o) ~= o then
source = mp.get_property(string.format("track-list/%d/external-filename", i))
aid = "auto"
end
break
end
end
local cmd = {
"run",
"mpv",
source,
"--loop-file=no",
"--video=no",
"--no-ocopy-metadata",
"--no-sub",
"--audio-channels=1",
string.format("--start=%.3f", s),
string.format("--length=%.3f", t),
string.format("--aid=%s", aid),
string.format("--volume=%s", USE_MPV_VOLUME and mp.get_property("volume") or "100"),
string.format("--af-append=afade=t=in:curve=ipar:st=%.3f:d=%.3f", s, AUDIO_CLIP_FADE),
string.format("--af-append=afade=t=out:curve=ipar:st=%.3f:d=%.3f", s + t - AUDIO_CLIP_FADE, AUDIO_CLIP_FADE),
string.format("-o=%s", destination),
}
mp.commandv(table.unpack(cmd))
dlog(utils.to_string(cmd))
end
local function create_screenshot(s, e)
local source = mp.get_property("path")
local img = utils.join_path(prefix, get_name(s, e) .. "." .. IMAGE_FORMAT)
local cmd = {
"run",
"mpv",
source,
"--loop-file=no",
"--audio=no",
"--no-ocopy-metadata",
"--no-sub",
"--frames=1",
}
if IMAGE_FORMAT == "webp" then
table.insert(cmd, "--ovc=libwebp")
table.insert(cmd, "--ovcopts-add=lossless=0")
table.insert(cmd, "--ovcopts-add=compression_level=6")
table.insert(cmd, "--ovcopts-add=preset=drawing")
elseif IMAGE_FORMAT == "png" then
table.insert(cmd, "--vf-add=format=rgb24")
end
table.insert(cmd, "--vf-add=scale=480*iw*sar/ih:480")
table.insert(cmd, string.format("--start=%.3f", mp.get_property_number("time-pos")))
table.insert(cmd, string.format("-o=%s", img))
mp.commandv(table.unpack(cmd))
dlog(utils.to_string(cmd))
end
local function add_to_last_added(ifield, afield, tfield)
local added_notes = anki_connect("findNotes", { query = "added:1" })["result"]
table.sort(added_notes)
local noteid = added_notes[#added_notes]
local note = anki_connect("notesInfo", { notes = { noteid } })
if note ~= nil then
local word = note["result"][1]["fields"][FRONT_FIELD]["value"]
local new_fields = {
[SENTENCE_AUDIO_FIELD] = afield,
[SENTENCE_FIELD] = tfield,
[IMAGE_FIELD] = ifield,
}
anki_connect("updateNoteFields", {
note = {
id = noteid,
fields = new_fields,
},
})
mp.osd_message("Updated note: " .. word, 3)
msg.info("Updated note: " .. word)
end
end
local function get_extract()
local lines = get_clipboard()
local e = 0
local s = 0
for line in lines:gmatch("[^\r\n]+") do
line = clean(line)
dlog(line)
if subs[line] ~= nil then
if subs[line][1] ~= nil and subs[line][2] ~= nil then
if s == 0 then
s = subs[line][1]
else
s = math.min(s, subs[line][1])
end
e = math.max(e, subs[line][2])
end
else
mp.osd_message("ERR! Line not found: " .. line, 3)
return
end
end
dlog(string.format("s=%d, e=%d", s, e))
if e ~= 0 then
create_screenshot(s, e)
create_audio(s, e)
local ifield = "<img src=" .. get_name(s, e) .. "." .. IMAGE_FORMAT .. ">"
local afield = "[sound:" .. get_name(s, e) .. ".mp3]"
local tfield = string.gsub(string.gsub(lines, "\n+", "<br />"), "\r", "")
add_to_last_added(ifield, afield, tfield)
if AUTOPLAY_AUDIO then
local name = get_name(s, e)
local audio = utils.join_path(prefix, name .. ".mp3")
local cmd = { "run", "mpv", audio, "--loop-file=no", "--load-scripts=no" }
mp.commandv(table.unpack(cmd))
end
end
end
local function ex()
if not prefix or prefix == "" then
set_media_dir()
end
if debug_mode then
get_extract()
else
pcall(get_extract)
end
end
local function rec(...)
if debug_mode then
record_sub(...)
else
pcall(record_sub, ...)
end
end
local function toggle_sub_to_clipboard()
ENABLE_SUBS_TO_CLIP = not ENABLE_SUBS_TO_CLIP
mp.osd_message("Clipboard inserter " .. (ENABLE_SUBS_TO_CLIP and "activated" or "deactived"), 3)
end
local function toggle_debug_mode()
debug_mode = not debug_mode
mp.osd_message("Debug mode " .. (debug_mode and "activated" or "deactived"), 3)
end
local function clear_subs(_)
subs = {}
end
mp.observe_property("sub-text", "string", rec)
mp.observe_property("filename", "string", clear_subs)
mp.add_key_binding("ctrl+v", "update-anki-card", ex)
mp.add_key_binding("ctrl+t", "toggle-clipboard-insertion", toggle_sub_to_clipboard)
mp.add_key_binding("ctrl+d", "toggle-debug-mode", toggle_debug_mode)
mp.add_key_binding("ctrl+V", ex)
mp.add_key_binding("ctrl+T", toggle_sub_to_clipboard)
mp.add_key_binding("ctrl+D", toggle_debug_mode)