From 7df9682378e90aa1fad86079b0bbeaaf0258c334 Mon Sep 17 00:00:00 2001
From: sudacode <suda@sudacode.com>
Date: Thu, 27 Mar 2025 21:11:03 -0700
Subject: [PATCH] replace immersive with animecard

---
 .gitmodules                             |   3 -
 mpv-anilist-updater                     |   1 -
 script-opts/immersive-dictionaries.conf |  71 ----
 script-opts/immersive-keys.conf         |   4 -
 script-opts/immersive-series.conf       |  12 -
 script-opts/immersive-style.conf        | 143 -------
 script-opts/immersive-targets.conf      | 139 -------
 script-opts/immersive.conf              |  82 ----
 scripts/animecards.lua                  | 496 ++++++++++++++++++++++++
 scripts/immersive                       |   1 -
 10 files changed, 496 insertions(+), 456 deletions(-)
 delete mode 160000 mpv-anilist-updater
 delete mode 100644 script-opts/immersive-dictionaries.conf
 delete mode 100644 script-opts/immersive-keys.conf
 delete mode 100644 script-opts/immersive-series.conf
 delete mode 100644 script-opts/immersive-style.conf
 delete mode 100644 script-opts/immersive-targets.conf
 delete mode 100644 script-opts/immersive.conf
 create mode 100644 scripts/animecards.lua
 delete mode 160000 scripts/immersive

diff --git a/.gitmodules b/.gitmodules
index 8f41a50..af0f685 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,6 +16,3 @@
 [submodule "scripts/autosubsync-mpv"]
 	path = scripts/autosubsync-mpv
 	url = git@github.com:Ajatt-Tools/autosubsync-mpv.git
-[submodule "scripts/immersive"]
-	path = scripts/immersive
-	url = git@github.com:Ben-Kerman/immersive.git
diff --git a/mpv-anilist-updater b/mpv-anilist-updater
deleted file mode 160000
index 6cc573e..0000000
--- a/mpv-anilist-updater
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 6cc573ec7dadaec4acca58b7e1b91f86c8e54e95
diff --git a/script-opts/immersive-dictionaries.conf b/script-opts/immersive-dictionaries.conf
deleted file mode 100644
index fe2b64c..0000000
--- a/script-opts/immersive-dictionaries.conf
+++ /dev/null
@@ -1,71 +0,0 @@
-# this line is here so the file is encoded as UTF−8
-# do not delete it unless you know what that means
-
-# example dictionary config for JMdict
-
-[JMdict]
-location=/home/sudacode/Documents/jmdict/
-type=yomichan
-
-
-# Only the two entries above are strictly required for each dictionary.
-# optional entries:
-
-# cf. doc/lookup-transformations.md, empty by default:
-#transformations=deinflect-japanese,deinflect-migaku(ja.json),kana
-
-# cf. doc/dictionaries.md for details
-#preload=<...>
-#insert_cjk_breaks=no
-exporter=default
-quick_def_template={{readings:::・}}{{variants:【:】:・}}: {{definitions:::; }}
-
-export:digits=0123456789
-export:reading_template={{reading}}{{variants:【:】:・}}
-export:definition_template={{tags:<span style="font-size\: 0.8em">:</span><br>:, }}{{num}}. {{keywords:::; }}
-export:template={{readings[1]}}:{{readings[2:] (:): }}<br>{{definitions:::<br>}}
-export:use_single_template=false
-export:single_template={{readings[1]}}:{{readings[2:] (:): }}<br>{{definitions:::<br>}}
-
-# --------------------
-
-# example config for Daijirin, probably usable for any EPWING exported by
-# yomichan-import
-
-# also set 'definition_substitutions=\<br><\n' in your target config
-# to add HTML line breaks to the definition
-
-#[大辞林]
-#location=<set to dictionary directory>
-#type=yomichan
-
-# to make text more readable in mpv
-#insert_cjk_breaks=yes
-
-# there is only one definition for EPWINGs exported to Yomichan
-# and it already includes the word
-#quick_def_template={{definitions}}
-
-# same as for quick_def_template
-#export:definition_template={{keywords}}
-#export:template={{definitions}}
-
-# these aren't needed since the reading and word are already in the definition
-#export:reading_template=
-#export:use_single_template=no
-
-# --------------------
-
-# example config for a Migaku dictionary
-
-#[Migaku Dictionary]
-#location=<set to path to dictionary JSON file>
-#type=migaku
-#exporter=default
-#quick_def_template={{definitions}}
-#export:template=[[
-#{{terms[1]}}{{terms[2:] (:):, }}:<br>
-#{{altterms::<br>:, }}{{pronunciations::<br>:, }}{{positions::<br>:, }}
-#{{definition}}
-#{{examples:::, }}
-#]]
diff --git a/script-opts/immersive-keys.conf b/script-opts/immersive-keys.conf
deleted file mode 100644
index 014cd57..0000000
--- a/script-opts/immersive-keys.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-open_global_menu=ctrl+i
-show_dict_target=ctrl+I
-export_active_line_instant=ctrl+e
-export_active_line_menua=ctrl+E
diff --git a/script-opts/immersive-series.conf b/script-opts/immersive-series.conf
deleted file mode 100644
index f5941ac..0000000
--- a/script-opts/immersive-series.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-# this line is here so the file is encoded as UTF−8
-# do not delete it unless you know what that means
-
-## streamed video; something similar should work for any platform youtube-dl supports
-#[youtube]
-#title={{media_title}}
-#keywords=youtu be
-#
-## local video file
-#[kaguya-sama]
-#title=かぐや様は告らせたい
-#keywords=kaguya sama kokurasetai
diff --git a/script-opts/immersive-style.conf b/script-opts/immersive-style.conf
deleted file mode 100644
index 61f1a40..0000000
--- a/script-opts/immersive-style.conf
+++ /dev/null
@@ -1,143 +0,0 @@
-# this line is here so the file is encoded as UTF−8
-# do not delete it unless you know what that means
-
-# Global entries serve as the basis for all other styles.
-#align           =5
-#bold            =<from mpv property 'osd-bold'>
-#italic          =<from mpv property 'osd-italic'>
-#underline       =no
-#strikeout       =no
-#border          =<from mpv property 'osd-border-size'>
-#border_x        =<from mpv property 'osd-border-size'>
-#border_y        =<from mpv property 'osd-border-size'>
-#shadow          =<from mpv property 'osd-shadow-offset'>
-#shadow_x        =<from mpv property 'osd-shadow-offset'>
-#shadow_y        =<from mpv property 'osd-shadow-offset'>
-#blur            =<from mpv property 'osd-blur'>
-#font_name       =<from mpv property 'osd-font'>
-#font_size       =30
-#letter_spacing  =<from mpv property 'osd-spacing'>
-#primary_color   =<from mpv property 'osd-color'>
-#secondary_color =808080
-#border_color    =<from mpv property 'osd-border-color'>
-#shadow_color    =<from mpv property 'osd-shadow-color'>
-#all_alpha       =FF
-#primary_alpha   =<from mpv property 'osd-color'>
-#secondary_alpha =00
-#border_alpha    =<from mpv property 'osd-border-color'>
-#shadow_alpha    =<from mpv property 'osd-shadow-color'>
-
-# --------------------
-
-# message log at the top right
-#[messages]
-#align=9
-
-#[messages/fatal]
-#bold=yes
-#primary_color=5791F9
-
-#[messages/error]
-#primary_color=7A77F2
-
-#[messages/warn]
-#primary_color=66CCFF
-
-#[messages/info]
-# none
-
-#[messages/verbose]
-#primary_color=99CC99
-
-#[messages/debug]
-#primary_color=A09F93
-
-#[messages/trace]
-# none
-
-# --------------------
-
-# menu help ("Press h to show key bindings")
-#[menu_help]
-#align=7
-
-# key bindings
-#[menu_help/key]
-#bold=yes
-
-# top line of the menu help
-#[menu_help/hint]
-#italic=yes
-
-# --------------------
-
-# menu info (timings, active target, etc.)
-#[menu_info]
-#align=1
-
-# description of an info item
-#[menu_info/key]
-#bold=yes
-
-# unset/unknown/automatically generated values
-#[menu_info/unset]
-#italic=yes
-
-# --------------------
-
-# line selection
-#[line_select]
-# none
-
-# actively selected line
-#[line_select/selection]
-#bold=yes
-#primary_color=FFD0D0
-
-# --------------------
-
-# text selection
-# applied on top of line_select during target selection
-#[text_select]
-# none
-
-# selected text
-#[text_select/selection]
-#primary_color=FF8080
-
-# --------------------
-
-# Forvo audio selection
-# applied on top of line_select
-#[word_audio_select]
-# none
-
-# pronunciations that have not been loaded yet
-#[word_audio_select/unloaded]
-#primary_color=808080
-
-# pronunciations that are currently loading
-#[word_audio_select/loading]
-#primary_color=8080FF
-
-# pronunciations that are ready to play
-#[word_audio_select/loaded]
-# none
-
-# --------------------
-
-# overlay of selected subtitles during subtitle selection
-#[selection_overlay]
-#align=3
-
-# --------------------
-
-# overlay shown when Immersive is blocked, e.g. while importing dictionaries
-#[info_overlay]
-#align=1
-
-
-# overlay for hiding the video during and after target selection
-#[blackout]
-#primary_color=<from mpv property 'background'>
-#primary_alpha=<from mpv property 'background'>
diff --git a/script-opts/immersive-targets.conf b/script-opts/immersive-targets.conf
deleted file mode 100644
index 62dcf70..0000000
--- a/script-opts/immersive-targets.conf
+++ /dev/null
@@ -1,139 +0,0 @@
-# this line is here so the file is encoded as UTF−8
-# do not delete it unless you know what that means
-
-[target name]
-# Anki profile the target will use
-# Can be taken from the window title of the main Anki window or from the profile
-# menu (Ctrl+Shift+P in Anki)
-profile=sudacode
-
-# Anki deck the target will use
-# Subdecks use the same syntax as in Anki itself
-# e.g. Root::Subdeck::Subsubdeck
-deck=Minecraft
-
-# note type the target will use
-note_type=Lapis
-
-# --------------------
-
-# example field definitions
-
-# uncomment (remove the #) and change so they fit your note type
-# Anki field names go between 'field:' and '=', exactly as they are
-# in Anki, including spaces.
-# Template variables come after the '=', like in the examples.
-
-#field:Front={{sentences}}
-#field:Back={{definitions}}
-#field:Word={{words[1]}}
-#field:Words={{words::: }}
-#field:Audio={{audio}}
-#field:Word Audio={{word_audio}}
-#field:Image={{image}}
-
-field:SentenceAudio={{audio}}
-field:Picture={{image}}
-field:MiscInfo={{series_title}}
-
-# Everything below this line has default values and is not required.
-# It is recommended to change image/max_width or image/max_height in order to
-# reduce file sizes, however.
-# --------------------
-
-# how export data will be added to existing notes
-# allowed values: 'append', 'prepend', 'overwrite'
-# overwrite replaces fields (but cf. template variable {{prev_content}})
-add_mode=append
-
-# template used for formatting notes within mpv
-# when selecting which existing note to export to
-#note_template={{type}}: {{id}}
-
-# Anki media dir override, optional and normally derived from system default
-# Must be an absolute path to the directory that encoded images/audio clips
-# should be placed in (i.e., to collection.media).
-#media_directory=<unset>
-
-# space-separated list of tags that will be added to exported notes
-#tags=immersive
-
-# can be set dynamically using field template variables, e.g.:
-#tags={{series_id}}
-
-# --------------------
-
-# substitutions to apply to the {{sentences}} variable
-# for more information, see doc/card-export.md
-#sentence_substitutions=[[
-#<(.-%)
-#<(.-%)
-#]]
-
-# same as sentence_substitutions but for {{definitions}}
-#definition_substitutions=
-
-# --------------------
-
-# audio clip file extension
-# unrelated to the format used, but should match it (especially on Windows)
-audio/extension=mp3
-
-# audio container format
-# e.g. 'matroska' (MKV/MKA), 'ogg', 'mp3'
-audio/format=mp3
-
-# audio codec
-# e.g. 'libopus' (NOT 'opus'), 'aac', 'vorbis', 'libmp3lame' (MP3)
-audio/codec=libmp3lame
-
-# audio bitrate
-# Uses the same syntax as mpv/ffmpeg bitrates.
-# Sensible values are 32ki-64ki for libopus and 128ki for AAC and MP3.
-audio/bitrate=128ki
-
-# how many seconds of padding to include before the start of audio clips
-#audio/pad_start=0.1
-
-# same as above, but after the end of the clip
-#audio/pad_end=0.1
-
-# --------------------
-
-# image file extension
-image/extension=webp
-
-# image codec
-# supported values are 'mjpeg' (JPG), 'libwebp' (WebP), and 'png'
-# Technically, any codec that works with ffmpeg's image2 format can be used.
-image/codec=libwebp
-
-# maximum image width/height
-# If one option is set to a negative value aspect ratio will be preserved.
-# If both are negative the video's resolution will be used.
-#image/max_width=-1
-#image/max_height=-1
-
-# quality of JPG (mjpeg) images
-# valid range: 1-69
-# lower is better (but files will be larger)
-# Values above 5-10 result in noticeable artifacting.
-image/jpeg/qscale=5
-
-# whether to use lossless compression for WebP
-image/webp/lossless=yes
-
-# libwebp quality factor
-# valid range: 0-100
-# higher is better
-image/webp/quality=90
-
-# libwebp compression level
-# valid range: 0-6
-# Higher values result in better compression but take longer to encode.
-image/webp/compression=4
-
-# PNG compression level
-# valid range: 0-9
-# higher is better
-#image/png/compression=9
diff --git a/script-opts/immersive.conf b/script-opts/immersive.conf
deleted file mode 100644
index f33b99f..0000000
--- a/script-opts/immersive.conf
+++ /dev/null
@@ -1,82 +0,0 @@
-# this line is here so the file is encoded as UTF−8
-# do not delete it unless you know what that means
-
-# the mpv executable to use for audio previews and encoding
-# if unset (default), the executable of the current process will be used
-#mpv_executable=<...>
-
-# if set to 'yes', load dictionaries when mpv starts
-preload_dictionaries=no
-
-# show the dictionary loading overlay when loading dicts at startup
-startup_dict_overlay=yes
-
-# maximum number of target words per card
-# set to 0 to disable limit
-#max_targets=1
-
-# always show minutes when displaying times,
-# even if playback has not reached 01:00 yet
-#always_show_minutes=yes
-
-# black out the screen during and after target selection
-#target_select_blackout=yes
-
-# black out the screen when looking up words from the active subtitle
-#active_sub_blackout=yes
-
-# language code to use when searching Forvo audio
-forvo_language=ja
-
-# Automatically load Forvo audio instead of waiting until attempting to play it.
-forvo_preload_audio=no
-
-# download mp3 files from Forvo instead of Ogg/Vorbis
-forvo_prefer_mp3=yes
-
-# prefix for Forvo filenames in the Anki media directory
-# Files will be named '<prefix>-<word>.<extension>'.
-#forvo_prefix=word_audio
-
-# reencode Forvo audio files since they are unnecessarily large
-forvo_reencode=yes
-
-# Forvo audio encoding options
-# These behave like the corresponding options in target configs.
-forvo_extension=mp3
-forvo_format=mp3
-forvo_codec=libmp3lame
-forvo_bitrate=128ki
-
-# AnkiConnect host and port
-ankiconnect_host=localhost
-ankiconnect_port=8765
-
-# Windows clipboard copy mode
-# "exact" takes longer (200ms-1s), but preserves the text exactly,
-# i.e. it also copies line breaks.
-# "quick" is much faster (<50ms), but might not copy the text
-# with 100% accuracy. This method sometimes causes encoding issues
-# and is known to sporadically copy corrupted text.
-#windows_copy_mode=exact
-
-# enable automatic subtitle copying by default
-#enable_autocopy=no
-
-# make subtitle autoselect toggle global instead of
-# being tied to each subtitle select menu
-#global_autoselect=yes
-
-# enable subtitle autoselect by default
-#enable_autoselect=yes
-
-# same as above but for the menu help toggle
-#global_help=yes
-#enable_help=no
-
-# enable screenshots by default
-take_screenshots=yes
-
-# Hide the menu info at the bottom left if the help overlay is active.
-# Useful if the two collide due to large font sizes.
-#hide_infos_if_help_active=no
diff --git a/scripts/animecards.lua b/scripts/animecards.lua
new file mode 100644
index 0000000..d546192
--- /dev/null
+++ b/scripts/animecards.lua
@@ -0,0 +1,496 @@
+------------- 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)
diff --git a/scripts/immersive b/scripts/immersive
deleted file mode 160000
index 9bde59a..0000000
--- a/scripts/immersive
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 9bde59a0abaa018096de5b6f38c3ad409b9cfdf1