------------- 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 = "Vocab" local SENTENCE_AUDIO_FIELD = "SentAudio" local SENTENCE_FIELD = "INVALID" -- the sentence is better taken care of by yomitan itself. local IMAGE_FIELD = "Image" -- 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 = true 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 < %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 = '' local afield = "[sound:".. get_name(s,e) .. ".mp3]" local tfield = string.gsub(string.gsub(lines,"\n+", "
"), "\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)