mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-02-27 12:22:43 -08:00
update
This commit is contained in:
@@ -78,7 +78,7 @@ exec-once = uwsm app -sb -t service -- nm-applet
|
|||||||
exec-once = uwsm app -sb -t service -- waybar -c ~/.config/waybar/catppuccin-macchiato/config.jsonc -s ~/.config/waybar/catppuccin-macchiato/style.css
|
exec-once = uwsm app -sb -t service -- waybar -c ~/.config/waybar/catppuccin-macchiato/config.jsonc -s ~/.config/waybar/catppuccin-macchiato/style.css
|
||||||
exec-once = uwsm app -sb -t service -- hyprsunset
|
exec-once = uwsm app -sb -t service -- hyprsunset
|
||||||
exec-once = uwsm app -sb -t service -- /usr/lib/polkit-kde-authentication-agent-1
|
exec-once = uwsm app -sb -t service -- /usr/lib/polkit-kde-authentication-agent-1
|
||||||
exec-once = uwsm app -sb -t service -- variety
|
# exec-once = uwsm app -sb -t service -- variety
|
||||||
exec-once = ~/.local/bin/aria
|
exec-once = ~/.local/bin/aria
|
||||||
# exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
|
# exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ profile-restore=copy-equal
|
|||||||
title=' '
|
title=' '
|
||||||
keepaspect=no
|
keepaspect=no
|
||||||
|
|
||||||
[immersion]
|
[subminer]
|
||||||
cookies=yes
|
cookies=yes
|
||||||
cookies-file=/truenas/sudacode/japanese/youtube-cookies.txt
|
cookies-file=/truenas/sudacode/japanese/youtube-cookies.txt
|
||||||
ytdl-format=bestvideo+bestaudio/best
|
ytdl-format=bestvideo+bestaudio/best
|
||||||
@@ -157,6 +157,9 @@ ytdl-raw-options-append=write-auto-subs=
|
|||||||
ytdl-raw-options-append=sub-langs=ja.*|en|ja-en
|
ytdl-raw-options-append=sub-langs=ja.*|en|ja-en
|
||||||
ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/youtube-cookies.txt
|
ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/youtube-cookies.txt
|
||||||
sub-auto=fuzzy
|
sub-auto=fuzzy
|
||||||
|
sid=auto
|
||||||
|
secondary-sid=auto
|
||||||
|
secondary-sub-visibility=no
|
||||||
alang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
alang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
||||||
slang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
slang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
||||||
vlang=ja,jpn
|
vlang=ja,jpn
|
||||||
|
|||||||
@@ -1,405 +0,0 @@
|
|||||||
----------------------
|
|
||||||
-- #example ytdl_preload.conf
|
|
||||||
-- # make sure lines do not have trailing whitespace
|
|
||||||
-- # ytdl_opt has no sanity check and should be formatted exactly how it would appear in yt-dlp CLI, they are split into a key/value pair on whitespace
|
|
||||||
-- # at least on Windows, do not escape '\' in temp, just us a single one for each divider
|
|
||||||
|
|
||||||
-- #temp=R:\ytdltest
|
|
||||||
-- #ytdl_opt1=-r 50k
|
|
||||||
-- #ytdl_opt2=-N 5
|
|
||||||
-- #ytdl_opt#=etc
|
|
||||||
----------------------
|
|
||||||
local nextIndex
|
|
||||||
local caught = true
|
|
||||||
-- local pop = false
|
|
||||||
local ytdl = "yt-dlp"
|
|
||||||
local utils = require 'mp.utils'
|
|
||||||
|
|
||||||
local options = require 'mp.options'
|
|
||||||
local opts = {
|
|
||||||
temp = "/tmp/ytdl-preload",
|
|
||||||
ytdl_opt1 = "",
|
|
||||||
ytdl_opt2 = "",
|
|
||||||
ytdl_opt3 = "",
|
|
||||||
ytdl_opt4 = "",
|
|
||||||
ytdl_opt5 = "",
|
|
||||||
ytdl_opt6 = "",
|
|
||||||
ytdl_opt7 = "",
|
|
||||||
ytdl_opt8 = "",
|
|
||||||
ytdl_opt9 = "",
|
|
||||||
}
|
|
||||||
options.read_options(opts, "ytdl_preload")
|
|
||||||
local additionalOpts = {}
|
|
||||||
for k, v in pairs(opts) do
|
|
||||||
if k:find("ytdl_opt%d") and v ~= "" then
|
|
||||||
additionalOpts[k] = v
|
|
||||||
-- print("entry")
|
|
||||||
-- print(k .. v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local cachePath = opts.temp
|
|
||||||
|
|
||||||
local chapter_list = {}
|
|
||||||
local json = ""
|
|
||||||
local filesToDelete = {}
|
|
||||||
|
|
||||||
local function exists(file)
|
|
||||||
local ok, err, code = os.rename(file, file)
|
|
||||||
if not ok then
|
|
||||||
if code == 13 then -- Permission denied, but it exists
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ok, err
|
|
||||||
end
|
|
||||||
local function useNewLoadfile()
|
|
||||||
for _, c in pairs(mp.get_property_native("command-list")) do
|
|
||||||
if c["name"] == "loadfile" then
|
|
||||||
for _, a in pairs(c["args"]) do
|
|
||||||
if a["name"] == "index" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--from ytdl_hook
|
|
||||||
local function time_to_secs(time_string)
|
|
||||||
local ret
|
|
||||||
local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
|
|
||||||
if a ~= nil then
|
|
||||||
ret = (a * 3600 + b * 60 + c)
|
|
||||||
else
|
|
||||||
a, b = time_string:match("(%d%d?):(%d%d)")
|
|
||||||
if a ~= nil then
|
|
||||||
ret = (a * 60 + b)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
local function extract_chapters(data, video_length)
|
|
||||||
local ret = {}
|
|
||||||
for line in data:gmatch("[^\r\n]+") do
|
|
||||||
local time = time_to_secs(line)
|
|
||||||
if time and (time < video_length) then
|
|
||||||
table.insert(ret, { time = time, title = line })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(ret, function(a, b) return a.time < b.time end)
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
local function chapters()
|
|
||||||
if json.chapters then
|
|
||||||
for i = 1, #json.chapters do
|
|
||||||
local chapter = json.chapters[i]
|
|
||||||
local title = chapter.title or ""
|
|
||||||
if title == "" then
|
|
||||||
title = string.format('Chapter %02d', i)
|
|
||||||
end
|
|
||||||
table.insert(chapter_list, { time = chapter.start_time, title = title })
|
|
||||||
end
|
|
||||||
elseif not (json.description == nil) and not (json.duration == nil) then
|
|
||||||
chapter_list = extract_chapters(json.description, json.duration)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--end ytdl_hook
|
|
||||||
local title = ""
|
|
||||||
local fVideo = ""
|
|
||||||
local fAudio = ""
|
|
||||||
local function load_files(dtitle, destination, audio, wait)
|
|
||||||
if wait then
|
|
||||||
if exists(destination .. ".mka") then
|
|
||||||
print("---wait success: found mka---")
|
|
||||||
audio = "audio-file=" .. destination .. '.mka,'
|
|
||||||
else
|
|
||||||
print("---could not find mka after wait, audio may be missing---")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- if audio ~= "" then
|
|
||||||
-- table.insert(filesToDelete, destination .. ".mka")
|
|
||||||
-- end
|
|
||||||
-- table.insert(filesToDelete, destination .. ".mkv")
|
|
||||||
dtitle = dtitle:gsub("-" .. ("[%w_-]"):rep(11) .. "$", "")
|
|
||||||
dtitle = dtitle:gsub("^" .. ("%d"):rep(10) .. "%-", "")
|
|
||||||
if useNewLoadfile() then
|
|
||||||
mp.commandv("loadfile", destination .. ".mkv", "append", -1,
|
|
||||||
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no')
|
|
||||||
else
|
|
||||||
mp.commandv("loadfile", destination .. ".mkv", "append",
|
|
||||||
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no') --,sub-file="..destination..".en.vtt") --in case they are not set up to autoload
|
|
||||||
end
|
|
||||||
mp.commandv("playlist_move", mp.get_property("playlist-count") - 1, nextIndex)
|
|
||||||
mp.commandv("playlist_remove", nextIndex + 1)
|
|
||||||
caught = true
|
|
||||||
title = ""
|
|
||||||
-- pop = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local listenID = ""
|
|
||||||
local function listener(event)
|
|
||||||
if not caught and event.prefix == mp.get_script_name() and string.find(event.text, listenID) then
|
|
||||||
local destination = string.match(event.text, "%[download%] Destination: (.+).mkv") or
|
|
||||||
string.match(event.text, "%[download%] (.+).mkv has already been downloaded")
|
|
||||||
-- if destination then print("---"..cachePath) end;
|
|
||||||
if destination and string.find(destination, string.gsub(cachePath, '~/', '')) then
|
|
||||||
-- print(listenID)
|
|
||||||
mp.unregister_event(listener)
|
|
||||||
_, title = utils.split_path(destination)
|
|
||||||
local audio = ""
|
|
||||||
if fAudio == "" then
|
|
||||||
load_files(title, destination, audio, false)
|
|
||||||
else
|
|
||||||
if exists(destination .. ".mka") then
|
|
||||||
audio = "audio-file=" .. destination .. '.mka,'
|
|
||||||
load_files(title, destination, audio, false)
|
|
||||||
else
|
|
||||||
print("---expected mka but could not find it, waiting for 2 seconds---")
|
|
||||||
mp.add_timeout(2, function()
|
|
||||||
load_files(title, destination, audio, true)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--from ytdl_hook
|
|
||||||
mp.add_hook("on_preloaded", 10, function()
|
|
||||||
if string.find(mp.get_property("path"), cachePath) then
|
|
||||||
chapters()
|
|
||||||
if next(chapter_list) ~= nil then
|
|
||||||
mp.set_property_native("chapter-list", chapter_list)
|
|
||||||
chapter_list = {}
|
|
||||||
json = ""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
--end ytdl_hook
|
|
||||||
function dump(o)
|
|
||||||
if type(o) == 'table' then
|
|
||||||
local s = '{ '
|
|
||||||
for k, v in pairs(o) do
|
|
||||||
if type(k) ~= 'number' then k = '"' .. k .. '"' end
|
|
||||||
s = s .. '[' .. k .. '] = ' .. dump(v) .. ','
|
|
||||||
end
|
|
||||||
return s .. '} '
|
|
||||||
else
|
|
||||||
return tostring(o)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addOPTS(old)
|
|
||||||
for k, v in pairs(additionalOpts) do
|
|
||||||
-- print(k)
|
|
||||||
if string.find(v, "%s") then
|
|
||||||
for l, w in string.gmatch(v, "([-%w]+) (.+)") do
|
|
||||||
table.insert(old, l)
|
|
||||||
table.insert(old, w)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
table.insert(old, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- print(dump(old))
|
|
||||||
return old
|
|
||||||
end
|
|
||||||
|
|
||||||
local AudioDownloadHandle = {}
|
|
||||||
local VideoDownloadHandle = {}
|
|
||||||
local JsonDownloadHandle = {}
|
|
||||||
local function download_files(id, success, result, error)
|
|
||||||
if result.killed_by_us then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local jfile = cachePath .. "/" .. id .. ".json"
|
|
||||||
|
|
||||||
local jfileIO = io.open(jfile, "w")
|
|
||||||
jfileIO:write(result.stdout)
|
|
||||||
jfileIO:close()
|
|
||||||
json = utils.parse_json(result.stdout)
|
|
||||||
-- print(dump(json))
|
|
||||||
if json.requested_downloads[1].requested_formats ~= nil then
|
|
||||||
local args = { ytdl, "--no-continue", "-q", "-f", fAudio, "--restrict-filenames", "--no-playlist", "--no-part",
|
|
||||||
"-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mka", "--load-info-json", jfile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
AudioDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
playback_only = false
|
|
||||||
}, function()
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
fAudio = ""
|
|
||||||
fVideo = fVideo:gsub("bestvideo", "best")
|
|
||||||
fVideo = fVideo:gsub("bv", "best")
|
|
||||||
end
|
|
||||||
|
|
||||||
local args = { ytdl, "--no-continue", "-f", fVideo .. '/best', "--restrict-filenames", "--no-playlist",
|
|
||||||
"--no-part", "-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mkv", "--load-info-json", jfile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
VideoDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
playback_only = false
|
|
||||||
}, function()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function DL()
|
|
||||||
local index = tonumber(mp.get_property("playlist-pos"))
|
|
||||||
if mp.get_property("playlist/" .. index .. "/filename"):find("/videos$") and mp.get_property("playlist/" .. index + 1 .. "/filename"):find("/shorts$") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if tonumber(mp.get_property("playlist-pos-1")) > 0 and mp.get_property("playlist-pos-1") ~= mp.get_property("playlist-count") then
|
|
||||||
nextIndex = index + 1
|
|
||||||
local nextFile = mp.get_property("playlist/" .. nextIndex .. "/filename")
|
|
||||||
if nextFile and caught and nextFile:find("://", 0, false) then
|
|
||||||
caught = false
|
|
||||||
mp.enable_messages("info")
|
|
||||||
mp.register_event("log-message", listener)
|
|
||||||
local ytFormat = mp.get_property("ytdl-format")
|
|
||||||
fVideo = string.match(ytFormat, '(.+)%+.+//?') or 'bestvideo'
|
|
||||||
fAudio = string.match(ytFormat, '.+%+(.+)//?') or 'bestaudio'
|
|
||||||
-- print("start"..nextFile)
|
|
||||||
listenID = tostring(os.time())
|
|
||||||
local args = { ytdl, "--dump-single-json", "--no-simulate", "--skip-download",
|
|
||||||
"--restrict-filenames",
|
|
||||||
"--no-playlist", "--sub-lang", "en", "--write-sub", "--no-part", "-o",
|
|
||||||
cachePath .. "/" .. listenID .. "-%(title)s-%(id)s.%(ext)s", nextFile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
-- print(dump(args))
|
|
||||||
table.insert(filesToDelete, listenID)
|
|
||||||
JsonDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true,
|
|
||||||
playback_only = false
|
|
||||||
}, function(...)
|
|
||||||
download_files(listenID, ...)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clearCache()
|
|
||||||
-- print(pop)
|
|
||||||
|
|
||||||
--if pop == true then
|
|
||||||
mp.abort_async_command(AudioDownloadHandle)
|
|
||||||
mp.abort_async_command(VideoDownloadHandle)
|
|
||||||
mp.abort_async_command(JsonDownloadHandle)
|
|
||||||
-- for k, v in pairs(filesToDelete) do
|
|
||||||
-- print("remove: " .. v)
|
|
||||||
-- os.remove(v)
|
|
||||||
-- end
|
|
||||||
local ftd = io.open(cachePath .. "/temp.files", "a")
|
|
||||||
for k, v in pairs(filesToDelete) do
|
|
||||||
ftd:write(v .. "\n")
|
|
||||||
if package.config:sub(1, 1) ~= '/' then
|
|
||||||
os.execute('del /Q /F "' .. cachePath .. "\\" .. v .. '*"')
|
|
||||||
else
|
|
||||||
os.execute('rm -f ' .. cachePath .. "/" .. v .. "*")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ftd:close()
|
|
||||||
print('clear')
|
|
||||||
mp.command("quit")
|
|
||||||
--end
|
|
||||||
end
|
|
||||||
mp.add_hook("on_unload", 50, function()
|
|
||||||
-- mp.abort_async_command(AudioDownloadHandle)
|
|
||||||
-- mp.abort_async_command(VideoDownloadHandle)
|
|
||||||
mp.abort_async_command(JsonDownloadHandle)
|
|
||||||
mp.unregister_event(listener)
|
|
||||||
caught = true
|
|
||||||
listenID = "resetYtdlPreloadListener"
|
|
||||||
-- print(listenID)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local skipInitial
|
|
||||||
mp.observe_property("playlist-count", "number", function()
|
|
||||||
if skipInitial then
|
|
||||||
DL()
|
|
||||||
else
|
|
||||||
skipInitial = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
--from ytdl_hook
|
|
||||||
local platform_is_windows = (package.config:sub(1, 1) == "\\")
|
|
||||||
local o = {
|
|
||||||
exclude = "",
|
|
||||||
try_ytdl_first = false,
|
|
||||||
use_manifests = false,
|
|
||||||
all_formats = false,
|
|
||||||
force_all_formats = true,
|
|
||||||
ytdl_path = "",
|
|
||||||
}
|
|
||||||
local paths_to_search = { "yt-dlp", "yt-dlp_x86", "youtube-dl" }
|
|
||||||
--local options = require 'mp.options'
|
|
||||||
options.read_options(o, "ytdl_hook")
|
|
||||||
|
|
||||||
local separator = platform_is_windows and ";" or ":"
|
|
||||||
if o.ytdl_path:match("[^" .. separator .. "]") then
|
|
||||||
paths_to_search = {}
|
|
||||||
for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
|
|
||||||
table.insert(paths_to_search, path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function exec(args)
|
|
||||||
local ret = mp.command_native({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true
|
|
||||||
})
|
|
||||||
return ret.status, ret.stdout, ret, ret.killed_by_us
|
|
||||||
end
|
|
||||||
|
|
||||||
local msg = require 'mp.msg'
|
|
||||||
local command = {}
|
|
||||||
for _, path in pairs(paths_to_search) do
|
|
||||||
-- search for youtube-dl in mpv's config dir
|
|
||||||
local exesuf = platform_is_windows and ".exe" or ""
|
|
||||||
local ytdl_cmd = mp.find_config_file(path .. exesuf)
|
|
||||||
if ytdl_cmd then
|
|
||||||
msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
|
|
||||||
ytdl = ytdl_cmd
|
|
||||||
break
|
|
||||||
else
|
|
||||||
msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
|
|
||||||
--search in PATH
|
|
||||||
command[1] = path
|
|
||||||
es, json, result, aborted = exec(command)
|
|
||||||
if result.error_string == "init" then
|
|
||||||
msg.verbose("youtube-dl with path " .. path .. exesuf .. " not found in PATH or not enough permissions")
|
|
||||||
else
|
|
||||||
msg.verbose("Found youtube-dl with path " .. path .. exesuf .. " in PATH")
|
|
||||||
ytdl = path
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--end ytdl_hook
|
|
||||||
|
|
||||||
mp.register_event("start-file", DL)
|
|
||||||
mp.register_event("shutdown", clearCache)
|
|
||||||
local ftd = io.open(cachePath .. "/temp.files", "r")
|
|
||||||
while ftd ~= nil do
|
|
||||||
local line = ftd:read()
|
|
||||||
if line == nil or line == "" then
|
|
||||||
ftd:close()
|
|
||||||
io.open(cachePath .. "/temp.files", "w"):close()
|
|
||||||
break
|
|
||||||
end
|
|
||||||
-- print("DEL::"..line)
|
|
||||||
if package.config:sub(1, 1) ~= '/' then
|
|
||||||
os.execute('del /Q /F "' .. cachePath .. "\\" .. line .. '*" >nul 2>nul')
|
|
||||||
else
|
|
||||||
os.execute('rm -f ' .. cachePath .. "/" .. line .. "* &> /dev/null")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
1
.config/mpv/scripts/ytdl-preload.lua
Symbolic link
1
.config/mpv/scripts/ytdl-preload.lua
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
ytdl-preload.lua##os.Linux
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
----------------------
|
|
||||||
-- #example ytdl_preload.conf
|
|
||||||
-- # make sure lines do not have trailing whitespace
|
|
||||||
-- # ytdl_opt has no sanity check and should be formatted exactly how it would appear in yt-dlp CLI, they are split into a key/value pair on whitespace
|
|
||||||
-- # at least on Windows, do not escape '\' in temp, just us a single one for each divider
|
|
||||||
|
|
||||||
-- #temp=R:\ytdltest
|
|
||||||
-- #ytdl_opt1=-r 50k
|
|
||||||
-- #ytdl_opt2=-N 5
|
|
||||||
-- #ytdl_opt#=etc
|
|
||||||
----------------------
|
|
||||||
local nextIndex
|
|
||||||
local caught = true
|
|
||||||
-- local pop = false
|
|
||||||
local ytdl = "yt-dlp"
|
|
||||||
local utils = require 'mp.utils'
|
|
||||||
|
|
||||||
local options = require 'mp.options'
|
|
||||||
local opts = {
|
|
||||||
temp = "/tmp/ytdl-preload",
|
|
||||||
ytdl_opt1 = "",
|
|
||||||
ytdl_opt2 = "",
|
|
||||||
ytdl_opt3 = "",
|
|
||||||
ytdl_opt4 = "",
|
|
||||||
ytdl_opt5 = "",
|
|
||||||
ytdl_opt6 = "",
|
|
||||||
ytdl_opt7 = "",
|
|
||||||
ytdl_opt8 = "",
|
|
||||||
ytdl_opt9 = "",
|
|
||||||
}
|
|
||||||
options.read_options(opts, "ytdl_preload")
|
|
||||||
local additionalOpts = {}
|
|
||||||
for k, v in pairs(opts) do
|
|
||||||
if k:find("ytdl_opt%d") and v ~= "" then
|
|
||||||
additionalOpts[k] = v
|
|
||||||
-- print("entry")
|
|
||||||
-- print(k .. v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local cachePath = opts.temp
|
|
||||||
|
|
||||||
local chapter_list = {}
|
|
||||||
local json = ""
|
|
||||||
local filesToDelete = {}
|
|
||||||
|
|
||||||
local function exists(file)
|
|
||||||
local ok, err, code = os.rename(file, file)
|
|
||||||
if not ok then
|
|
||||||
if code == 13 then -- Permission denied, but it exists
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ok, err
|
|
||||||
end
|
|
||||||
local function useNewLoadfile()
|
|
||||||
for _, c in pairs(mp.get_property_native("command-list")) do
|
|
||||||
if c["name"] == "loadfile" then
|
|
||||||
for _, a in pairs(c["args"]) do
|
|
||||||
if a["name"] == "index" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--from ytdl_hook
|
|
||||||
local function time_to_secs(time_string)
|
|
||||||
local ret
|
|
||||||
local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
|
|
||||||
if a ~= nil then
|
|
||||||
ret = (a * 3600 + b * 60 + c)
|
|
||||||
else
|
|
||||||
a, b = time_string:match("(%d%d?):(%d%d)")
|
|
||||||
if a ~= nil then
|
|
||||||
ret = (a * 60 + b)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
local function extract_chapters(data, video_length)
|
|
||||||
local ret = {}
|
|
||||||
for line in data:gmatch("[^\r\n]+") do
|
|
||||||
local time = time_to_secs(line)
|
|
||||||
if time and (time < video_length) then
|
|
||||||
table.insert(ret, { time = time, title = line })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(ret, function(a, b) return a.time < b.time end)
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
local function chapters()
|
|
||||||
if json.chapters then
|
|
||||||
for i = 1, #json.chapters do
|
|
||||||
local chapter = json.chapters[i]
|
|
||||||
local title = chapter.title or ""
|
|
||||||
if title == "" then
|
|
||||||
title = string.format('Chapter %02d', i)
|
|
||||||
end
|
|
||||||
table.insert(chapter_list, { time = chapter.start_time, title = title })
|
|
||||||
end
|
|
||||||
elseif not (json.description == nil) and not (json.duration == nil) then
|
|
||||||
chapter_list = extract_chapters(json.description, json.duration)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--end ytdl_hook
|
|
||||||
local title = ""
|
|
||||||
local fVideo = ""
|
|
||||||
local fAudio = ""
|
|
||||||
local function load_files(dtitle, destination, audio, wait)
|
|
||||||
if wait then
|
|
||||||
if exists(destination .. ".mka") then
|
|
||||||
print("---wait success: found mka---")
|
|
||||||
audio = "audio-file=" .. destination .. '.mka,'
|
|
||||||
else
|
|
||||||
print("---could not find mka after wait, audio may be missing---")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- if audio ~= "" then
|
|
||||||
-- table.insert(filesToDelete, destination .. ".mka")
|
|
||||||
-- end
|
|
||||||
-- table.insert(filesToDelete, destination .. ".mkv")
|
|
||||||
dtitle = dtitle:gsub("-" .. ("[%w_-]"):rep(11) .. "$", "")
|
|
||||||
dtitle = dtitle:gsub("^" .. ("%d"):rep(10) .. "%-", "")
|
|
||||||
if useNewLoadfile() then
|
|
||||||
mp.commandv("loadfile", destination .. ".mkv", "append", -1,
|
|
||||||
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no')
|
|
||||||
else
|
|
||||||
mp.commandv("loadfile", destination .. ".mkv", "append",
|
|
||||||
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no') --,sub-file="..destination..".en.vtt") --in case they are not set up to autoload
|
|
||||||
end
|
|
||||||
mp.commandv("playlist_move", mp.get_property("playlist-count") - 1, nextIndex)
|
|
||||||
mp.commandv("playlist_remove", nextIndex + 1)
|
|
||||||
caught = true
|
|
||||||
title = ""
|
|
||||||
-- pop = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local listenID = ""
|
|
||||||
local function listener(event)
|
|
||||||
if not caught and event.prefix == mp.get_script_name() and string.find(event.text, listenID) then
|
|
||||||
local destination = string.match(event.text, "%[download%] Destination: (.+).mkv") or
|
|
||||||
string.match(event.text, "%[download%] (.+).mkv has already been downloaded")
|
|
||||||
-- if destination then print("---"..cachePath) end;
|
|
||||||
if destination and string.find(destination, string.gsub(cachePath, '~/', '')) then
|
|
||||||
-- print(listenID)
|
|
||||||
mp.unregister_event(listener)
|
|
||||||
_, title = utils.split_path(destination)
|
|
||||||
local audio = ""
|
|
||||||
if fAudio == "" then
|
|
||||||
load_files(title, destination, audio, false)
|
|
||||||
else
|
|
||||||
if exists(destination .. ".mka") then
|
|
||||||
audio = "audio-file=" .. destination .. '.mka,'
|
|
||||||
load_files(title, destination, audio, false)
|
|
||||||
else
|
|
||||||
print("---expected mka but could not find it, waiting for 2 seconds---")
|
|
||||||
mp.add_timeout(2, function()
|
|
||||||
load_files(title, destination, audio, true)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--from ytdl_hook
|
|
||||||
mp.add_hook("on_preloaded", 10, function()
|
|
||||||
if string.find(mp.get_property("path"), cachePath) then
|
|
||||||
chapters()
|
|
||||||
if next(chapter_list) ~= nil then
|
|
||||||
mp.set_property_native("chapter-list", chapter_list)
|
|
||||||
chapter_list = {}
|
|
||||||
json = ""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
--end ytdl_hook
|
|
||||||
function dump(o)
|
|
||||||
if type(o) == 'table' then
|
|
||||||
local s = '{ '
|
|
||||||
for k, v in pairs(o) do
|
|
||||||
if type(k) ~= 'number' then k = '"' .. k .. '"' end
|
|
||||||
s = s .. '[' .. k .. '] = ' .. dump(v) .. ','
|
|
||||||
end
|
|
||||||
return s .. '} '
|
|
||||||
else
|
|
||||||
return tostring(o)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addOPTS(old)
|
|
||||||
for k, v in pairs(additionalOpts) do
|
|
||||||
-- print(k)
|
|
||||||
if string.find(v, "%s") then
|
|
||||||
for l, w in string.gmatch(v, "([-%w]+) (.+)") do
|
|
||||||
table.insert(old, l)
|
|
||||||
table.insert(old, w)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
table.insert(old, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- print(dump(old))
|
|
||||||
return old
|
|
||||||
end
|
|
||||||
|
|
||||||
local AudioDownloadHandle = {}
|
|
||||||
local VideoDownloadHandle = {}
|
|
||||||
local JsonDownloadHandle = {}
|
|
||||||
local function download_files(id, success, result, error)
|
|
||||||
if result.killed_by_us then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local jfile = cachePath .. "/" .. id .. ".json"
|
|
||||||
|
|
||||||
local jfileIO = io.open(jfile, "w")
|
|
||||||
jfileIO:write(result.stdout)
|
|
||||||
jfileIO:close()
|
|
||||||
json = utils.parse_json(result.stdout)
|
|
||||||
-- print(dump(json))
|
|
||||||
if json.requested_downloads[1].requested_formats ~= nil then
|
|
||||||
local args = { ytdl, "--no-continue", "-q", "-f", fAudio, "--restrict-filenames", "--no-playlist", "--no-part",
|
|
||||||
"-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mka", "--load-info-json", jfile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
AudioDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
playback_only = false
|
|
||||||
}, function()
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
fAudio = ""
|
|
||||||
fVideo = fVideo:gsub("bestvideo", "best")
|
|
||||||
fVideo = fVideo:gsub("bv", "best")
|
|
||||||
end
|
|
||||||
|
|
||||||
local args = { ytdl, "--no-continue", "-f", fVideo .. '/best', "--restrict-filenames", "--no-playlist",
|
|
||||||
"--no-part", "-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mkv", "--load-info-json", jfile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
VideoDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
playback_only = false
|
|
||||||
}, function()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function DL()
|
|
||||||
local index = tonumber(mp.get_property("playlist-pos"))
|
|
||||||
if mp.get_property("playlist/" .. index .. "/filename"):find("/videos$") and mp.get_property("playlist/" .. index + 1 .. "/filename"):find("/shorts$") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if tonumber(mp.get_property("playlist-pos-1")) > 0 and mp.get_property("playlist-pos-1") ~= mp.get_property("playlist-count") then
|
|
||||||
nextIndex = index + 1
|
|
||||||
local nextFile = mp.get_property("playlist/" .. nextIndex .. "/filename")
|
|
||||||
if nextFile and caught and nextFile:find("://", 0, false) then
|
|
||||||
caught = false
|
|
||||||
mp.enable_messages("info")
|
|
||||||
mp.register_event("log-message", listener)
|
|
||||||
local ytFormat = mp.get_property("ytdl-format")
|
|
||||||
fVideo = string.match(ytFormat, '(.+)%+.+//?') or 'bestvideo'
|
|
||||||
fAudio = string.match(ytFormat, '.+%+(.+)//?') or 'bestaudio'
|
|
||||||
-- print("start"..nextFile)
|
|
||||||
listenID = tostring(os.time())
|
|
||||||
local args = { ytdl, "--dump-single-json", "--no-simulate", "--skip-download",
|
|
||||||
"--restrict-filenames",
|
|
||||||
"--no-playlist", "--sub-lang", "en", "--write-sub", "--no-part", "-o",
|
|
||||||
cachePath .. "/" .. listenID .. "-%(title)s-%(id)s.%(ext)s", nextFile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
-- print(dump(args))
|
|
||||||
table.insert(filesToDelete, listenID)
|
|
||||||
JsonDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true,
|
|
||||||
playback_only = false
|
|
||||||
}, function(...)
|
|
||||||
download_files(listenID, ...)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clearCache()
|
|
||||||
-- print(pop)
|
|
||||||
|
|
||||||
--if pop == true then
|
|
||||||
mp.abort_async_command(AudioDownloadHandle)
|
|
||||||
mp.abort_async_command(VideoDownloadHandle)
|
|
||||||
mp.abort_async_command(JsonDownloadHandle)
|
|
||||||
-- for k, v in pairs(filesToDelete) do
|
|
||||||
-- print("remove: " .. v)
|
|
||||||
-- os.remove(v)
|
|
||||||
-- end
|
|
||||||
local ftd = io.open(cachePath .. "/temp.files", "a")
|
|
||||||
for k, v in pairs(filesToDelete) do
|
|
||||||
ftd:write(v .. "\n")
|
|
||||||
if package.config:sub(1, 1) ~= '/' then
|
|
||||||
os.execute('del /Q /F "' .. cachePath .. "\\" .. v .. '*"')
|
|
||||||
else
|
|
||||||
os.execute('rm -f ' .. cachePath .. "/" .. v .. "*")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ftd:close()
|
|
||||||
print('clear')
|
|
||||||
mp.command("quit")
|
|
||||||
--end
|
|
||||||
end
|
|
||||||
mp.add_hook("on_unload", 50, function()
|
|
||||||
-- mp.abort_async_command(AudioDownloadHandle)
|
|
||||||
-- mp.abort_async_command(VideoDownloadHandle)
|
|
||||||
mp.abort_async_command(JsonDownloadHandle)
|
|
||||||
mp.unregister_event(listener)
|
|
||||||
caught = true
|
|
||||||
listenID = "resetYtdlPreloadListener"
|
|
||||||
-- print(listenID)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local skipInitial
|
|
||||||
mp.observe_property("playlist-count", "number", function()
|
|
||||||
if skipInitial then
|
|
||||||
DL()
|
|
||||||
else
|
|
||||||
skipInitial = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
--from ytdl_hook
|
|
||||||
local platform_is_windows = (package.config:sub(1, 1) == "\\")
|
|
||||||
local o = {
|
|
||||||
exclude = "",
|
|
||||||
try_ytdl_first = false,
|
|
||||||
use_manifests = false,
|
|
||||||
all_formats = false,
|
|
||||||
force_all_formats = true,
|
|
||||||
ytdl_path = "",
|
|
||||||
}
|
|
||||||
local paths_to_search = { "yt-dlp", "yt-dlp_x86", "youtube-dl" }
|
|
||||||
--local options = require 'mp.options'
|
|
||||||
options.read_options(o, "ytdl_hook")
|
|
||||||
|
|
||||||
local separator = platform_is_windows and ";" or ":"
|
|
||||||
if o.ytdl_path:match("[^" .. separator .. "]") then
|
|
||||||
paths_to_search = {}
|
|
||||||
for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
|
|
||||||
table.insert(paths_to_search, path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function exec(args)
|
|
||||||
local ret = mp.command_native({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true
|
|
||||||
})
|
|
||||||
return ret.status, ret.stdout, ret, ret.killed_by_us
|
|
||||||
end
|
|
||||||
|
|
||||||
local msg = require 'mp.msg'
|
|
||||||
local command = {}
|
|
||||||
for _, path in pairs(paths_to_search) do
|
|
||||||
-- search for youtube-dl in mpv's config dir
|
|
||||||
local exesuf = platform_is_windows and ".exe" or ""
|
|
||||||
local ytdl_cmd = mp.find_config_file(path .. exesuf)
|
|
||||||
if ytdl_cmd then
|
|
||||||
msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
|
|
||||||
ytdl = ytdl_cmd
|
|
||||||
break
|
|
||||||
else
|
|
||||||
msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
|
|
||||||
--search in PATH
|
|
||||||
command[1] = path
|
|
||||||
es, json, result, aborted = exec(command)
|
|
||||||
if result.error_string == "init" then
|
|
||||||
msg.verbose("youtube-dl with path " .. path .. exesuf .. " not found in PATH or not enough permissions")
|
|
||||||
else
|
|
||||||
msg.verbose("Found youtube-dl with path " .. path .. exesuf .. " in PATH")
|
|
||||||
ytdl = path
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--end ytdl_hook
|
|
||||||
|
|
||||||
mp.register_event("start-file", DL)
|
|
||||||
mp.register_event("shutdown", clearCache)
|
|
||||||
local ftd = io.open(cachePath .. "/temp.files", "r")
|
|
||||||
while ftd ~= nil do
|
|
||||||
local line = ftd:read()
|
|
||||||
if line == nil or line == "" then
|
|
||||||
ftd:close()
|
|
||||||
io.open(cachePath .. "/temp.files", "w"):close()
|
|
||||||
break
|
|
||||||
end
|
|
||||||
-- print("DEL::"..line)
|
|
||||||
if package.config:sub(1, 1) ~= '/' then
|
|
||||||
os.execute('del /Q /F "' .. cachePath .. "\\" .. line .. '*" >nul 2>nul')
|
|
||||||
else
|
|
||||||
os.execute('rm -f ' .. cachePath .. "/" .. line .. "* &> /dev/null")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
----------------------
|
|
||||||
-- #example ytdl_preload.conf
|
|
||||||
-- # make sure lines do not have trailing whitespace
|
|
||||||
-- # ytdl_opt has no sanity check and should be formatted exactly how it would appear in yt-dlp CLI, they are split into a key/value pair on whitespace
|
|
||||||
-- # at least on Windows, do not escape '\' in temp, just us a single one for each divider
|
|
||||||
|
|
||||||
-- #temp=R:\ytdltest
|
|
||||||
-- #ytdl_opt1=-r 50k
|
|
||||||
-- #ytdl_opt2=-N 5
|
|
||||||
-- #ytdl_opt#=etc
|
|
||||||
----------------------
|
|
||||||
local nextIndex
|
|
||||||
local caught = true
|
|
||||||
-- local pop = false
|
|
||||||
local ytdl = "yt-dlp"
|
|
||||||
local utils = require 'mp.utils'
|
|
||||||
|
|
||||||
local options = require 'mp.options'
|
|
||||||
local opts = {
|
|
||||||
temp = "/tmp/ytdl-preload",
|
|
||||||
ytdl_opt1 = "",
|
|
||||||
ytdl_opt2 = "",
|
|
||||||
ytdl_opt3 = "",
|
|
||||||
ytdl_opt4 = "",
|
|
||||||
ytdl_opt5 = "",
|
|
||||||
ytdl_opt6 = "",
|
|
||||||
ytdl_opt7 = "",
|
|
||||||
ytdl_opt8 = "",
|
|
||||||
ytdl_opt9 = "",
|
|
||||||
}
|
|
||||||
options.read_options(opts, "ytdl_preload")
|
|
||||||
local additionalOpts = {}
|
|
||||||
for k, v in pairs(opts) do
|
|
||||||
if k:find("ytdl_opt%d") and v ~= "" then
|
|
||||||
additionalOpts[k] = v
|
|
||||||
-- print("entry")
|
|
||||||
-- print(k .. v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local cachePath = opts.temp
|
|
||||||
|
|
||||||
local chapter_list = {}
|
|
||||||
local json = ""
|
|
||||||
local filesToDelete = {}
|
|
||||||
|
|
||||||
local function exists(file)
|
|
||||||
local ok, err, code = os.rename(file, file)
|
|
||||||
if not ok then
|
|
||||||
if code == 13 then -- Permission denied, but it exists
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ok, err
|
|
||||||
end
|
|
||||||
local function useNewLoadfile()
|
|
||||||
for _, c in pairs(mp.get_property_native("command-list")) do
|
|
||||||
if c["name"] == "loadfile" then
|
|
||||||
for _, a in pairs(c["args"]) do
|
|
||||||
if a["name"] == "index" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--from ytdl_hook
|
|
||||||
local function time_to_secs(time_string)
|
|
||||||
local ret
|
|
||||||
local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
|
|
||||||
if a ~= nil then
|
|
||||||
ret = (a * 3600 + b * 60 + c)
|
|
||||||
else
|
|
||||||
a, b = time_string:match("(%d%d?):(%d%d)")
|
|
||||||
if a ~= nil then
|
|
||||||
ret = (a * 60 + b)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
local function extract_chapters(data, video_length)
|
|
||||||
local ret = {}
|
|
||||||
for line in data:gmatch("[^\r\n]+") do
|
|
||||||
local time = time_to_secs(line)
|
|
||||||
if time and (time < video_length) then
|
|
||||||
table.insert(ret, { time = time, title = line })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(ret, function(a, b) return a.time < b.time end)
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
local function chapters()
|
|
||||||
if json.chapters then
|
|
||||||
for i = 1, #json.chapters do
|
|
||||||
local chapter = json.chapters[i]
|
|
||||||
local title = chapter.title or ""
|
|
||||||
if title == "" then
|
|
||||||
title = string.format('Chapter %02d', i)
|
|
||||||
end
|
|
||||||
table.insert(chapter_list, { time = chapter.start_time, title = title })
|
|
||||||
end
|
|
||||||
elseif not (json.description == nil) and not (json.duration == nil) then
|
|
||||||
chapter_list = extract_chapters(json.description, json.duration)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--end ytdl_hook
|
|
||||||
local title = ""
|
|
||||||
local fVideo = ""
|
|
||||||
local fAudio = ""
|
|
||||||
local function load_files(dtitle, destination, audio, wait)
|
|
||||||
if wait then
|
|
||||||
if exists(destination .. ".mka") then
|
|
||||||
print("---wait success: found mka---")
|
|
||||||
audio = "audio-file=" .. destination .. '.mka,'
|
|
||||||
else
|
|
||||||
print("---could not find mka after wait, audio may be missing---")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- if audio ~= "" then
|
|
||||||
-- table.insert(filesToDelete, destination .. ".mka")
|
|
||||||
-- end
|
|
||||||
-- table.insert(filesToDelete, destination .. ".mkv")
|
|
||||||
dtitle = dtitle:gsub("-" .. ("[%w_-]"):rep(11) .. "$", "")
|
|
||||||
dtitle = dtitle:gsub("^" .. ("%d"):rep(10) .. "%-", "")
|
|
||||||
if useNewLoadfile() then
|
|
||||||
mp.commandv("loadfile", destination .. ".mkv", "append", -1,
|
|
||||||
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no')
|
|
||||||
else
|
|
||||||
mp.commandv("loadfile", destination .. ".mkv", "append",
|
|
||||||
audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no') --,sub-file="..destination..".en.vtt") --in case they are not set up to autoload
|
|
||||||
end
|
|
||||||
mp.commandv("playlist_move", mp.get_property("playlist-count") - 1, nextIndex)
|
|
||||||
mp.commandv("playlist_remove", nextIndex + 1)
|
|
||||||
caught = true
|
|
||||||
title = ""
|
|
||||||
-- pop = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local listenID = ""
|
|
||||||
local function listener(event)
|
|
||||||
if not caught and event.prefix == mp.get_script_name() and string.find(event.text, listenID) then
|
|
||||||
local destination = string.match(event.text, "%[download%] Destination: (.+).mkv") or
|
|
||||||
string.match(event.text, "%[download%] (.+).mkv has already been downloaded")
|
|
||||||
-- if destination then print("---"..cachePath) end;
|
|
||||||
if destination and string.find(destination, string.gsub(cachePath, '~/', '')) then
|
|
||||||
-- print(listenID)
|
|
||||||
mp.unregister_event(listener)
|
|
||||||
_, title = utils.split_path(destination)
|
|
||||||
local audio = ""
|
|
||||||
if fAudio == "" then
|
|
||||||
load_files(title, destination, audio, false)
|
|
||||||
else
|
|
||||||
if exists(destination .. ".mka") then
|
|
||||||
audio = "audio-file=" .. destination .. '.mka,'
|
|
||||||
load_files(title, destination, audio, false)
|
|
||||||
else
|
|
||||||
print("---expected mka but could not find it, waiting for 2 seconds---")
|
|
||||||
mp.add_timeout(2, function()
|
|
||||||
load_files(title, destination, audio, true)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--from ytdl_hook
|
|
||||||
mp.add_hook("on_preloaded", 10, function()
|
|
||||||
if string.find(mp.get_property("path"), cachePath) then
|
|
||||||
chapters()
|
|
||||||
if next(chapter_list) ~= nil then
|
|
||||||
mp.set_property_native("chapter-list", chapter_list)
|
|
||||||
chapter_list = {}
|
|
||||||
json = ""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
--end ytdl_hook
|
|
||||||
function dump(o)
|
|
||||||
if type(o) == 'table' then
|
|
||||||
local s = '{ '
|
|
||||||
for k, v in pairs(o) do
|
|
||||||
if type(k) ~= 'number' then k = '"' .. k .. '"' end
|
|
||||||
s = s .. '[' .. k .. '] = ' .. dump(v) .. ','
|
|
||||||
end
|
|
||||||
return s .. '} '
|
|
||||||
else
|
|
||||||
return tostring(o)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addOPTS(old)
|
|
||||||
for k, v in pairs(additionalOpts) do
|
|
||||||
-- print(k)
|
|
||||||
if string.find(v, "%s") then
|
|
||||||
for l, w in string.gmatch(v, "([-%w]+) (.+)") do
|
|
||||||
table.insert(old, l)
|
|
||||||
table.insert(old, w)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
table.insert(old, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- print(dump(old))
|
|
||||||
return old
|
|
||||||
end
|
|
||||||
|
|
||||||
local AudioDownloadHandle = {}
|
|
||||||
local VideoDownloadHandle = {}
|
|
||||||
local JsonDownloadHandle = {}
|
|
||||||
local function download_files(id, success, result, error)
|
|
||||||
if result.killed_by_us then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local jfile = cachePath .. "/" .. id .. ".json"
|
|
||||||
|
|
||||||
local jfileIO = io.open(jfile, "w")
|
|
||||||
jfileIO:write(result.stdout)
|
|
||||||
jfileIO:close()
|
|
||||||
json = utils.parse_json(result.stdout)
|
|
||||||
-- print(dump(json))
|
|
||||||
if json.requested_downloads[1].requested_formats ~= nil then
|
|
||||||
local args = { ytdl, "--no-continue", "-q", "-f", fAudio, "--restrict-filenames", "--no-playlist", "--no-part",
|
|
||||||
"-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mka", "--load-info-json", jfile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
AudioDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
playback_only = false
|
|
||||||
}, function()
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
fAudio = ""
|
|
||||||
fVideo = fVideo:gsub("bestvideo", "best")
|
|
||||||
fVideo = fVideo:gsub("bv", "best")
|
|
||||||
end
|
|
||||||
|
|
||||||
local args = { ytdl, "--no-continue", "-f", fVideo .. '/best', "--restrict-filenames", "--no-playlist",
|
|
||||||
"--no-part", "-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mkv", "--load-info-json", jfile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
VideoDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
playback_only = false
|
|
||||||
}, function()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function DL()
|
|
||||||
local index = tonumber(mp.get_property("playlist-pos"))
|
|
||||||
if mp.get_property("playlist/" .. index .. "/filename"):find("/videos$") and mp.get_property("playlist/" .. index + 1 .. "/filename"):find("/shorts$") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if tonumber(mp.get_property("playlist-pos-1")) > 0 and mp.get_property("playlist-pos-1") ~= mp.get_property("playlist-count") then
|
|
||||||
nextIndex = index + 1
|
|
||||||
local nextFile = mp.get_property("playlist/" .. nextIndex .. "/filename")
|
|
||||||
if nextFile and caught and nextFile:find("://", 0, false) then
|
|
||||||
caught = false
|
|
||||||
mp.enable_messages("info")
|
|
||||||
mp.register_event("log-message", listener)
|
|
||||||
local ytFormat = mp.get_property("ytdl-format")
|
|
||||||
fVideo = string.match(ytFormat, '(.+)%+.+//?') or 'bestvideo'
|
|
||||||
fAudio = string.match(ytFormat, '.+%+(.+)//?') or 'bestaudio'
|
|
||||||
-- print("start"..nextFile)
|
|
||||||
listenID = tostring(os.time())
|
|
||||||
local args = { ytdl, "--dump-single-json", "--no-simulate", "--skip-download",
|
|
||||||
"--restrict-filenames",
|
|
||||||
"--no-playlist", "--sub-lang", "en", "--write-sub", "--no-part", "-o",
|
|
||||||
cachePath .. "/" .. listenID .. "-%(title)s-%(id)s.%(ext)s", nextFile }
|
|
||||||
args = addOPTS(args)
|
|
||||||
-- print(dump(args))
|
|
||||||
table.insert(filesToDelete, listenID)
|
|
||||||
JsonDownloadHandle = mp.command_native_async({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true,
|
|
||||||
playback_only = false
|
|
||||||
}, function(...)
|
|
||||||
download_files(listenID, ...)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function clearCache()
|
|
||||||
-- print(pop)
|
|
||||||
|
|
||||||
--if pop == true then
|
|
||||||
mp.abort_async_command(AudioDownloadHandle)
|
|
||||||
mp.abort_async_command(VideoDownloadHandle)
|
|
||||||
mp.abort_async_command(JsonDownloadHandle)
|
|
||||||
-- for k, v in pairs(filesToDelete) do
|
|
||||||
-- print("remove: " .. v)
|
|
||||||
-- os.remove(v)
|
|
||||||
-- end
|
|
||||||
local ftd = io.open(cachePath .. "/temp.files", "a")
|
|
||||||
for k, v in pairs(filesToDelete) do
|
|
||||||
ftd:write(v .. "\n")
|
|
||||||
if package.config:sub(1, 1) ~= '/' then
|
|
||||||
os.execute('del /Q /F "' .. cachePath .. "\\" .. v .. '*"')
|
|
||||||
else
|
|
||||||
os.execute('rm -f ' .. cachePath .. "/" .. v .. "*")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ftd:close()
|
|
||||||
print('clear')
|
|
||||||
mp.command("quit")
|
|
||||||
--end
|
|
||||||
end
|
|
||||||
mp.add_hook("on_unload", 50, function()
|
|
||||||
-- mp.abort_async_command(AudioDownloadHandle)
|
|
||||||
-- mp.abort_async_command(VideoDownloadHandle)
|
|
||||||
mp.abort_async_command(JsonDownloadHandle)
|
|
||||||
mp.unregister_event(listener)
|
|
||||||
caught = true
|
|
||||||
listenID = "resetYtdlPreloadListener"
|
|
||||||
-- print(listenID)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local skipInitial
|
|
||||||
mp.observe_property("playlist-count", "number", function()
|
|
||||||
if skipInitial then
|
|
||||||
DL()
|
|
||||||
else
|
|
||||||
skipInitial = true
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
--from ytdl_hook
|
|
||||||
local platform_is_windows = (package.config:sub(1, 1) == "\\")
|
|
||||||
local o = {
|
|
||||||
exclude = "",
|
|
||||||
try_ytdl_first = false,
|
|
||||||
use_manifests = false,
|
|
||||||
all_formats = false,
|
|
||||||
force_all_formats = true,
|
|
||||||
ytdl_path = "",
|
|
||||||
}
|
|
||||||
local paths_to_search = { "yt-dlp", "yt-dlp_x86", "youtube-dl" }
|
|
||||||
--local options = require 'mp.options'
|
|
||||||
options.read_options(o, "ytdl_hook")
|
|
||||||
|
|
||||||
local separator = platform_is_windows and ";" or ":"
|
|
||||||
if o.ytdl_path:match("[^" .. separator .. "]") then
|
|
||||||
paths_to_search = {}
|
|
||||||
for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
|
|
||||||
table.insert(paths_to_search, path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function exec(args)
|
|
||||||
local ret = mp.command_native({
|
|
||||||
name = "subprocess",
|
|
||||||
args = args,
|
|
||||||
capture_stdout = true,
|
|
||||||
capture_stderr = true
|
|
||||||
})
|
|
||||||
return ret.status, ret.stdout, ret, ret.killed_by_us
|
|
||||||
end
|
|
||||||
|
|
||||||
local msg = require 'mp.msg'
|
|
||||||
local command = {}
|
|
||||||
for _, path in pairs(paths_to_search) do
|
|
||||||
-- search for youtube-dl in mpv's config dir
|
|
||||||
local exesuf = platform_is_windows and ".exe" or ""
|
|
||||||
local ytdl_cmd = mp.find_config_file(path .. exesuf)
|
|
||||||
if ytdl_cmd then
|
|
||||||
msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
|
|
||||||
ytdl = ytdl_cmd
|
|
||||||
break
|
|
||||||
else
|
|
||||||
msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
|
|
||||||
--search in PATH
|
|
||||||
command[1] = path
|
|
||||||
es, json, result, aborted = exec(command)
|
|
||||||
if result.error_string == "init" then
|
|
||||||
msg.verbose("youtube-dl with path " .. path .. exesuf .. " not found in PATH or not enough permissions")
|
|
||||||
else
|
|
||||||
msg.verbose("Found youtube-dl with path " .. path .. exesuf .. " in PATH")
|
|
||||||
ytdl = path
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--end ytdl_hook
|
|
||||||
|
|
||||||
mp.register_event("start-file", DL)
|
|
||||||
mp.register_event("shutdown", clearCache)
|
|
||||||
local ftd = io.open(cachePath .. "/temp.files", "r")
|
|
||||||
while ftd ~= nil do
|
|
||||||
local line = ftd:read()
|
|
||||||
if line == nil or line == "" then
|
|
||||||
ftd:close()
|
|
||||||
io.open(cachePath .. "/temp.files", "w"):close()
|
|
||||||
break
|
|
||||||
end
|
|
||||||
-- print("DEL::"..line)
|
|
||||||
if package.config:sub(1, 1) ~= '/' then
|
|
||||||
os.execute('del /Q /F "' .. cachePath .. "\\" .. line .. '*" >nul 2>nul')
|
|
||||||
else
|
|
||||||
os.execute('rm -f ' .. cachePath .. "/" .. line .. "* &> /dev/null")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
{
|
|
||||||
"keybindings": [],
|
|
||||||
"auto_start_overlay": false,
|
|
||||||
"texthooker": {
|
|
||||||
"openBrowser": false,
|
|
||||||
},
|
|
||||||
"websocket": {
|
|
||||||
"enabled": "auto",
|
|
||||||
"port": 6677,
|
|
||||||
},
|
|
||||||
"ankiConnect": {
|
|
||||||
"enabled": true,
|
|
||||||
"url": "http://127.0.0.1:8765",
|
|
||||||
"pollingRate": 500,
|
|
||||||
"fields": {
|
|
||||||
"audio": "ExpressionAudio",
|
|
||||||
"image": "Picture",
|
|
||||||
"sentence": "Sentence",
|
|
||||||
"miscInfo": "MiscInfo",
|
|
||||||
"translation": "SelectionText",
|
|
||||||
},
|
|
||||||
"openRouter": {
|
|
||||||
"enabled": true,
|
|
||||||
"alwaysUseAiTranslation": true,
|
|
||||||
"apiKey": "",
|
|
||||||
"model": "openai/gpt-oss-120b:free",
|
|
||||||
"baseUrl": "https://openrouter.ai/api/v1",
|
|
||||||
"sourceLanguage": "Japanese",
|
|
||||||
"systemPrompt": "You are a translation engine for translating Japanese into natural-sounding, context-aware English. Return only the translated text with no extra explanations or commentary. The translation must preserve the original tone and intent of the source. If the input is not in the target language, translate it to the target language. If the input is already in the target language, return it as is.",
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"generateAudio": true,
|
|
||||||
"generateImage": true,
|
|
||||||
"imageType": "avif",
|
|
||||||
"imageFormat": "webp",
|
|
||||||
"animatedFps": 24,
|
|
||||||
"animatedMaxWidth": 640,
|
|
||||||
"animatedMaxHeight": null,
|
|
||||||
"animatedCrf": 35,
|
|
||||||
"audioPadding": 0.5,
|
|
||||||
"fallbackDuration": 3,
|
|
||||||
},
|
|
||||||
"behavior": {
|
|
||||||
"overwriteAudio": false,
|
|
||||||
"overwriteImage": true,
|
|
||||||
"mediaInsertMode": "append",
|
|
||||||
"highlightWord": true,
|
|
||||||
"notificationType": "system",
|
|
||||||
"showNotificationOnUpdate": true,
|
|
||||||
"autoUpdateNewCards": false,
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"pattern": "[SubMiner] %f (%t)",
|
|
||||||
},
|
|
||||||
"isLapis": {
|
|
||||||
"enabled": true,
|
|
||||||
"sentenceCardModel": "Lapis Morph",
|
|
||||||
"sentenceCardSentenceField": "Sentence",
|
|
||||||
"sentenceCardAudioField": "SentenceAudio",
|
|
||||||
},
|
|
||||||
"isKiku": {
|
|
||||||
"enabled": true,
|
|
||||||
"fieldGrouping": "manual",
|
|
||||||
"deleteDuplicateInAuto": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"subtitles": {
|
|
||||||
"primarySubLanguages": ["ja", "jpn"],
|
|
||||||
"secondarySubLanguages": ["en", "eng"],
|
|
||||||
"autoLoadSecondarySub": true,
|
|
||||||
"defaultMode": "hover",
|
|
||||||
"style": {
|
|
||||||
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
|
|
||||||
"fontSize": 35,
|
|
||||||
"fontColor": "#cad3f5",
|
|
||||||
"fontWeight": "normal",
|
|
||||||
"fontStyle": "normal",
|
|
||||||
"backgroundColor": "rgba(54, 58, 79, 0.69)",
|
|
||||||
"secondary": {
|
|
||||||
"fontSize": 24,
|
|
||||||
"fontColor": "#ffffff",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"subsync": {
|
|
||||||
"defaultMode": "manual",
|
|
||||||
"alass_path": "/Users/sudacode/.local/bin/alass-cli",
|
|
||||||
"ffsubsync_path": "/Users/sudacode/.local/bin/ffsubsync",
|
|
||||||
"ffmpeg_path": "/opt/homebrew/bin/ffmpeg",
|
|
||||||
},
|
|
||||||
"subtitleStyle": {
|
|
||||||
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
|
|
||||||
"fontSize": 24,
|
|
||||||
"fontColor": "#cad3f5",
|
|
||||||
"fontWeight": "normal",
|
|
||||||
"fontStyle": "normal",
|
|
||||||
"backgroundColor": "rgb(30, 32, 48, 0.88)",
|
|
||||||
"secondary": {
|
|
||||||
"fontSize": 24,
|
|
||||||
"fontColor": "#cad3f5",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"jimaku": {
|
|
||||||
// "apiKey": "YOUR_API_KEY",
|
|
||||||
// or use a command that outputs the key:
|
|
||||||
"apiKeyCommand": "cat ~/.jimaku-api-key",
|
|
||||||
"apiBaseUrl": "https://jimaku.cc",
|
|
||||||
"languagePreference": "ja",
|
|
||||||
"maxEntryResults": 10,
|
|
||||||
},
|
|
||||||
"shortcuts": {
|
|
||||||
"copySubtitle": "CommandOrControl+C",
|
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C",
|
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V",
|
|
||||||
"triggerFieldGrouping": "CommandOrControl+G",
|
|
||||||
"triggerSubsync": "CommandOrControl+Alt+S",
|
|
||||||
"mineSentence": "CommandOrControl+S",
|
|
||||||
"mineSentenceMultiple": "CommandOrControl+Shift+S",
|
|
||||||
"multiCopyTimeoutMs": 3000,
|
|
||||||
"toggleSecondarySub": "CommandOrControl+Shift+V",
|
|
||||||
"markAudioCard": "CommandOrControl+Shift+A",
|
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ export XDG_CONFIG_HOME=$HOME/.config
|
|||||||
export COMPOSE_BAKE=true
|
export COMPOSE_BAKE=true
|
||||||
export ANKI_WAYLAND=1
|
export ANKI_WAYLAND=1
|
||||||
export SUDO_PROMPT=$'\a[sudo] password for %u: '
|
export SUDO_PROMPT=$'\a[sudo] password for %u: '
|
||||||
|
export ELECTRON_OZONE_PLATFORM_HINT=x11
|
||||||
|
|
||||||
# nvidia
|
# nvidia
|
||||||
export NVD_BACKEND=direct
|
export NVD_BACKEND=direct
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"custom/mpd-scroll",
|
"custom/mpd-scroll",
|
||||||
"custom/mpv-scroll",
|
"custom/mpv-scroll",
|
||||||
"custom/firefox-scroll",
|
"custom/firefox-scroll",
|
||||||
"hyprland/submap"
|
"hyprland/submap",
|
||||||
],
|
],
|
||||||
// "modules-center": ["hyprland/window"],
|
// "modules-center": ["hyprland/window"],
|
||||||
"modules-center": ["custom/notification"],
|
"modules-center": ["custom/notification"],
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"network",
|
"network",
|
||||||
"pulseaudio",
|
"pulseaudio",
|
||||||
"clock",
|
"clock",
|
||||||
"custom/weather",
|
"custom/weather"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hyprland/workspaces": {
|
"hyprland/workspaces": {
|
||||||
|
|||||||
@@ -10,6 +10,31 @@ source ~/.environment
|
|||||||
|
|
||||||
source <(fzf --zsh)
|
source <(fzf --zsh)
|
||||||
|
|
||||||
|
fzf-file-widget-smart-root() {
|
||||||
|
setopt localoptions pipefail no_aliases 2>/dev/null
|
||||||
|
|
||||||
|
local -a words
|
||||||
|
local last raw root sel
|
||||||
|
words=(${(z)LBUFFER})
|
||||||
|
last=${words[-1]}
|
||||||
|
raw=${(Q)last} # unquote shell word
|
||||||
|
root=${~raw} # expand ~
|
||||||
|
|
||||||
|
if [[ -n "$raw" && "$raw" == */ && -d "$root" ]]; then
|
||||||
|
sel="$(cd -- "$root" && __fzf_select)" || return
|
||||||
|
LBUFFER+="$sel"
|
||||||
|
else
|
||||||
|
LBUFFER+="$(__fzf_select)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
zle reset-prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N fzf-file-widget-smart-root
|
||||||
|
bindkey -M emacs '^T' fzf-file-widget-smart-root
|
||||||
|
bindkey -M vicmd '^T' fzf-file-widget-smart-root
|
||||||
|
bindkey -M viins '^T' fzf-file-widget-smart-root
|
||||||
|
|
||||||
eval $(thefuck --alias)
|
eval $(thefuck --alias)
|
||||||
|
|
||||||
HISTFILE=~/.zsh_history
|
HISTFILE=~/.zsh_history
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
THEME="${THEME:-/opt/mpv-yomitan/catppuccin-macchiato.rasi}"
|
THEME="${THEME:-$HOME/.local/share/SubMiner/themes/subminer.rasi}"
|
||||||
FONTCONFIG_FILE=$HOME/.config/mpv/mpv-fonts.conf
|
FONTCONFIG_FILE=$HOME/.config/mpv/mpv-fonts.conf
|
||||||
COMMAND=mpv
|
COMMAND=mpv
|
||||||
VIDEO_EXTENSIONS="mkv|mp4|avi|webm|mov|flv|wmv|m4v|ts|m2ts"
|
VIDEO_EXTENSIONS="mkv|mp4|avi|webm|mov|flv|wmv|m4v|ts|m2ts"
|
||||||
|
|
||||||
# Parse command-line options first
|
# Parse command-line options first
|
||||||
while getopts ":it:" opt; do
|
while getopts ":st:" opt; do
|
||||||
case $opt in
|
case $opt in
|
||||||
i)
|
s)
|
||||||
COMMAND="$COMMAND --profile=immersion"
|
COMMAND="$COMMAND --profile=subminer"
|
||||||
;;
|
;;
|
||||||
t)
|
t)
|
||||||
THEME="$OPTARG"
|
THEME="$OPTARG"
|
||||||
|
|||||||
@@ -1,28 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Record microphone audio and transcribe it with whisper.cpp."""
|
"""Record microphone audio and transcribe it with whisper.cpp or faster-whisper."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import wave
|
import wave
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
|
|
||||||
DEFAULT_MODEL = "~/models/whisper.cpp/ggml-small.bin"
|
DEFAULT_MODEL = "small"
|
||||||
DEFAULT_DURATION = 8.0
|
DEFAULT_DURATION = 8.0
|
||||||
DEFAULT_STATE_DIR = Path.home() / ".cache" / "whisper-record-toggle"
|
DEFAULT_STATE_DIR = Path.home() / ".cache" / "whisper-record-toggle"
|
||||||
|
DEFAULT_WHISPERCPP_MODEL_DIR = Path.home() / "models" / "whisper.cpp"
|
||||||
APP_NAME = "Whisper Record"
|
APP_NAME = "Whisper Record"
|
||||||
|
DEFAULT_TOGGLE_DEBOUNCE = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _append_log(state_dir: Path, message: str) -> None:
|
||||||
|
state_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
log_file = state_dir / "worker.log"
|
||||||
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
with log_file.open("a", encoding="utf-8") as fh:
|
||||||
|
fh.write(f"[{timestamp}] {message}\n")
|
||||||
|
|
||||||
|
|
||||||
class Notifier:
|
class Notifier:
|
||||||
@@ -120,20 +129,6 @@ def _format_seconds(value: float) -> str:
|
|||||||
return f"{minutes:02d}:{seconds:02d}"
|
return f"{minutes:02d}:{seconds:02d}"
|
||||||
|
|
||||||
|
|
||||||
def find_whisper_binary(explicit: str | None) -> str:
|
|
||||||
if explicit:
|
|
||||||
return explicit
|
|
||||||
|
|
||||||
for candidate in ("whisper-cli", "main", "whisper"):
|
|
||||||
path = shutil.which(candidate)
|
|
||||||
if path:
|
|
||||||
return path
|
|
||||||
|
|
||||||
raise RuntimeError(
|
|
||||||
"Could not find whisper.cpp binary. Pass --whisper-bin /path/to/whisper-cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def write_wav(path: Path, audio: np.ndarray, samplerate: int, channels: int) -> None:
|
def write_wav(path: Path, audio: np.ndarray, samplerate: int, channels: int) -> None:
|
||||||
with wave.open(str(path), "wb") as wav_file:
|
with wave.open(str(path), "wb") as wav_file:
|
||||||
wav_file.setnchannels(channels)
|
wav_file.setnchannels(channels)
|
||||||
@@ -142,72 +137,164 @@ def write_wav(path: Path, audio: np.ndarray, samplerate: int, channels: int) ->
|
|||||||
wav_file.writeframes(audio.tobytes())
|
wav_file.writeframes(audio.tobytes())
|
||||||
|
|
||||||
|
|
||||||
def transcribe(whisper_bin: str, model: str, wav_path: Path, notifier: Notifier) -> str:
|
def transcribe(
|
||||||
with tempfile.TemporaryDirectory(prefix="whisper-out-") as out_dir:
|
backend: str,
|
||||||
out_base = Path(out_dir) / "transcript"
|
model_name_or_path: str,
|
||||||
|
wav_path: Path,
|
||||||
|
notifier: Notifier,
|
||||||
|
device: str,
|
||||||
|
compute_type: str,
|
||||||
|
beam_size: int,
|
||||||
|
) -> str:
|
||||||
|
if backend == "whispercpp":
|
||||||
|
return transcribe_whispercpp(
|
||||||
|
model_name_or_path=model_name_or_path,
|
||||||
|
wav_path=wav_path,
|
||||||
|
notifier=notifier,
|
||||||
|
device=device,
|
||||||
|
beam_size=beam_size,
|
||||||
|
)
|
||||||
|
if backend == "ctranslate2":
|
||||||
|
return transcribe_ctranslate2(
|
||||||
|
model_name_or_path=model_name_or_path,
|
||||||
|
wav_path=wav_path,
|
||||||
|
notifier=notifier,
|
||||||
|
device=device,
|
||||||
|
compute_type=compute_type,
|
||||||
|
beam_size=beam_size,
|
||||||
|
)
|
||||||
|
raise RuntimeError(f"Unsupported backend: {backend}")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_whispercpp_model(model_name_or_path: str) -> Path:
|
||||||
|
candidate = Path(model_name_or_path).expanduser()
|
||||||
|
if candidate.exists():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
name = model_name_or_path.strip()
|
||||||
|
search_paths = [
|
||||||
|
DEFAULT_WHISPERCPP_MODEL_DIR / name,
|
||||||
|
DEFAULT_WHISPERCPP_MODEL_DIR / f"ggml-{name}.bin",
|
||||||
|
DEFAULT_WHISPERCPP_MODEL_DIR / f"ggml-{name}.en.bin",
|
||||||
|
]
|
||||||
|
for path in search_paths:
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
"whisper.cpp model not found. Pass --model as a .bin path or place model at "
|
||||||
|
f"{DEFAULT_WHISPERCPP_MODEL_DIR}/ggml-<name>.bin (for example ggml-small.bin)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def transcribe_whispercpp(
|
||||||
|
model_name_or_path: str,
|
||||||
|
wav_path: Path,
|
||||||
|
notifier: Notifier,
|
||||||
|
device: str,
|
||||||
|
beam_size: int,
|
||||||
|
) -> str:
|
||||||
|
whisper_cli = shutil.which("whisper-cli")
|
||||||
|
if not whisper_cli:
|
||||||
|
raise RuntimeError("whisper-cli not found in PATH. Install whisper.cpp.")
|
||||||
|
|
||||||
|
model_path = _resolve_whispercpp_model(model_name_or_path)
|
||||||
|
output_prefix = wav_path.parent / wav_path.stem
|
||||||
|
output_txt = Path(f"{output_prefix}.txt")
|
||||||
|
if output_txt.exists():
|
||||||
|
output_txt.unlink()
|
||||||
|
|
||||||
|
notifier.send("Transcribing", "Running whisper.cpp...", timeout_ms=1500)
|
||||||
cmd = [
|
cmd = [
|
||||||
whisper_bin,
|
whisper_cli,
|
||||||
"-m",
|
|
||||||
model,
|
|
||||||
"-f",
|
"-f",
|
||||||
str(wav_path),
|
str(wav_path),
|
||||||
|
"-m",
|
||||||
|
str(model_path),
|
||||||
"-otxt",
|
"-otxt",
|
||||||
"-of",
|
"-of",
|
||||||
str(out_base),
|
str(output_prefix),
|
||||||
"-nt",
|
"-bs",
|
||||||
|
str(beam_size),
|
||||||
|
"-np",
|
||||||
]
|
]
|
||||||
process = subprocess.Popen(
|
if device == "cpu":
|
||||||
cmd,
|
cmd.append("-ng")
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
text=True,
|
if result.returncode != 0:
|
||||||
bufsize=1,
|
details = (result.stderr or result.stdout or "").strip()
|
||||||
|
raise RuntimeError(details or "whisper.cpp failed.")
|
||||||
|
if not output_txt.exists():
|
||||||
|
details = (result.stderr or result.stdout or "").strip()
|
||||||
|
raise RuntimeError(
|
||||||
|
"whisper.cpp completed but no transcript file was produced. "
|
||||||
|
f"Expected: {output_txt}. {details}"
|
||||||
)
|
)
|
||||||
output_lines: list[str] = []
|
return output_txt.read_text(encoding="utf-8").strip()
|
||||||
progress: dict[str, int | None] = {"pct": None}
|
|
||||||
|
|
||||||
def _reader() -> None:
|
|
||||||
assert process.stdout is not None
|
|
||||||
for line in process.stdout:
|
|
||||||
output_lines.append(line)
|
|
||||||
match = re.search(r"(?<!\d)(\d{1,3})%", line)
|
|
||||||
if match:
|
|
||||||
progress["pct"] = min(100, int(match.group(1)))
|
|
||||||
|
|
||||||
reader = threading.Thread(target=_reader, daemon=True)
|
def transcribe_ctranslate2(
|
||||||
reader.start()
|
model_name_or_path: str,
|
||||||
|
wav_path: Path,
|
||||||
spinner = "|/-\\"
|
notifier: Notifier,
|
||||||
frame = 0
|
device: str,
|
||||||
while process.poll() is None:
|
compute_type: str,
|
||||||
pct = progress["pct"]
|
beam_size: int,
|
||||||
status = (
|
) -> str:
|
||||||
f"Transcribing... {pct}%"
|
whisper_cli = shutil.which("whisper-ctranslate2")
|
||||||
if pct is not None
|
if not whisper_cli:
|
||||||
else f"Transcribing... {spinner[frame % len(spinner)]}"
|
raise RuntimeError(
|
||||||
|
"whisper-ctranslate2 not found in PATH. Install with: pip install faster-whisper"
|
||||||
)
|
)
|
||||||
notifier.send("Transcribing", status, timeout_ms=1200)
|
|
||||||
print(f"\r{status}", end="", file=sys.stderr, flush=True)
|
|
||||||
frame += 1
|
|
||||||
time.sleep(0.35)
|
|
||||||
|
|
||||||
reader.join(timeout=1.0)
|
if model_name_or_path.endswith(".bin"):
|
||||||
print("\r" + (" " * 48) + "\r", end="", file=sys.stderr, flush=True)
|
raise RuntimeError(
|
||||||
result_stdout = "".join(output_lines).strip()
|
"faster-whisper/ctranslate2 does not use ggml .bin models. "
|
||||||
|
"Use a model name like 'small' or a CTranslate2 model directory."
|
||||||
|
)
|
||||||
|
|
||||||
if process.returncode != 0:
|
notifier.send("Transcribing", "Running whisper-ctranslate2...", timeout_ms=1500)
|
||||||
stderr = result_stdout
|
output_dir = wav_path.parent
|
||||||
raise RuntimeError(f"whisper.cpp failed: {stderr}")
|
output_txt = output_dir / f"{wav_path.stem}.txt"
|
||||||
|
if output_txt.exists():
|
||||||
|
output_txt.unlink()
|
||||||
|
|
||||||
txt_file = out_base.with_suffix(".txt")
|
cmd = [
|
||||||
if txt_file.exists():
|
whisper_cli,
|
||||||
return txt_file.read_text(encoding="utf-8").strip()
|
str(wav_path),
|
||||||
|
"--output_dir",
|
||||||
|
str(output_dir),
|
||||||
|
"--output_format",
|
||||||
|
"txt",
|
||||||
|
"--device",
|
||||||
|
device,
|
||||||
|
"--compute_type",
|
||||||
|
compute_type,
|
||||||
|
"--beam_size",
|
||||||
|
str(beam_size),
|
||||||
|
"--verbose",
|
||||||
|
"False",
|
||||||
|
]
|
||||||
|
model_dir_candidate = Path(model_name_or_path).expanduser()
|
||||||
|
if model_dir_candidate.exists() and model_dir_candidate.is_dir():
|
||||||
|
cmd.extend(["--model_directory", str(model_dir_candidate)])
|
||||||
|
elif "/" in model_name_or_path or model_name_or_path.startswith("."):
|
||||||
|
cmd.extend(["--model_directory", model_name_or_path])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--model", model_name_or_path])
|
||||||
|
|
||||||
fallback = result_stdout
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
if fallback:
|
if result.returncode != 0:
|
||||||
return fallback
|
details = (result.stderr or result.stdout or "").strip()
|
||||||
|
raise RuntimeError(details or "whisper-ctranslate2 failed.")
|
||||||
raise RuntimeError("Transcription finished but no output text was produced.")
|
if not output_txt.exists():
|
||||||
|
details = (result.stderr or result.stdout or "").strip()
|
||||||
|
raise RuntimeError(
|
||||||
|
"whisper-ctranslate2 completed but no transcript file was produced. "
|
||||||
|
f"Expected: {output_txt}. {details}"
|
||||||
|
)
|
||||||
|
return output_txt.read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
def _type_with_tool(text: str) -> None:
|
def _type_with_tool(text: str) -> None:
|
||||||
@@ -261,13 +348,26 @@ def _is_alive(pid: int | None) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _read_and_clear_error(error_file: Path) -> str | None:
|
||||||
|
if not error_file.exists():
|
||||||
|
return None
|
||||||
|
message = error_file.read_text(encoding="utf-8").strip()
|
||||||
|
error_file.unlink()
|
||||||
|
return message or "Worker failed."
|
||||||
|
|
||||||
|
|
||||||
def _run_transcription_job(args: argparse.Namespace, duration: float | None) -> str:
|
def _run_transcription_job(args: argparse.Namespace, duration: float | None) -> str:
|
||||||
notifier = Notifier()
|
notifier = Notifier()
|
||||||
model_path = Path(args.model).expanduser()
|
model_name_or_path = args.model
|
||||||
|
if (
|
||||||
|
"/" in model_name_or_path
|
||||||
|
or model_name_or_path.startswith(".")
|
||||||
|
or model_name_or_path.startswith("~")
|
||||||
|
):
|
||||||
|
model_path = Path(model_name_or_path).expanduser()
|
||||||
if not model_path.exists():
|
if not model_path.exists():
|
||||||
raise RuntimeError(f"Model file not found: {model_path}")
|
raise RuntimeError(f"Model path not found: {model_path}")
|
||||||
|
model_name_or_path = str(model_path)
|
||||||
whisper_bin = find_whisper_binary(args.whisper_bin)
|
|
||||||
|
|
||||||
notifier.send("Recording", "Starting...", timeout_ms=1200)
|
notifier.send("Recording", "Starting...", timeout_ms=1200)
|
||||||
recorder = Recorder(samplerate=args.samplerate, channels=args.channels)
|
recorder = Recorder(samplerate=args.samplerate, channels=args.channels)
|
||||||
@@ -289,8 +389,20 @@ def _run_transcription_job(args: argparse.Namespace, duration: float | None) ->
|
|||||||
with tempfile.TemporaryDirectory(prefix="whisper-audio-") as tmp_dir:
|
with tempfile.TemporaryDirectory(prefix="whisper-audio-") as tmp_dir:
|
||||||
wav_path = Path(tmp_dir) / "input.wav"
|
wav_path = Path(tmp_dir) / "input.wav"
|
||||||
write_wav(wav_path, audio, args.samplerate, args.channels)
|
write_wav(wav_path, audio, args.samplerate, args.channels)
|
||||||
notifier.send("Transcribing", "Running whisper.cpp...", timeout_ms=1500)
|
notifier.send(
|
||||||
text = transcribe(whisper_bin, str(model_path), wav_path, notifier)
|
"Transcribing",
|
||||||
|
f"Running backend: {args.backend}",
|
||||||
|
timeout_ms=1500,
|
||||||
|
)
|
||||||
|
text = transcribe(
|
||||||
|
backend=args.backend,
|
||||||
|
model_name_or_path=model_name_or_path,
|
||||||
|
wav_path=wav_path,
|
||||||
|
notifier=notifier,
|
||||||
|
device=args.device,
|
||||||
|
compute_type=args.compute_type,
|
||||||
|
beam_size=args.beam_size,
|
||||||
|
)
|
||||||
|
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
@@ -324,12 +436,21 @@ def run_worker(args: argparse.Namespace) -> int:
|
|||||||
transcript_file.unlink()
|
transcript_file.unlink()
|
||||||
if error_file.exists():
|
if error_file.exists():
|
||||||
error_file.unlink()
|
error_file.unlink()
|
||||||
|
_append_log(
|
||||||
|
state_dir,
|
||||||
|
f"worker start model={args.model} device={args.device} compute_type={args.compute_type}",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
text = _run_transcription_job(args, duration=None)
|
text = _run_transcription_job(args, duration=None)
|
||||||
transcript_file.write_text(text, encoding="utf-8")
|
transcript_file.write_text(text, encoding="utf-8")
|
||||||
|
_append_log(state_dir, f"worker complete transcript_chars={len(text)}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
details = "".join(
|
||||||
|
traceback.format_exception(type(exc), exc, exc.__traceback__)
|
||||||
|
).strip()
|
||||||
error_file.write_text(str(exc), encoding="utf-8")
|
error_file.write_text(str(exc), encoding="utf-8")
|
||||||
|
_append_log(state_dir, f"worker error: {details}")
|
||||||
return 1
|
return 1
|
||||||
finally:
|
finally:
|
||||||
if pid_file.exists():
|
if pid_file.exists():
|
||||||
@@ -354,6 +475,8 @@ def start_background(args: argparse.Namespace) -> int:
|
|||||||
"--mode",
|
"--mode",
|
||||||
"once",
|
"once",
|
||||||
"--worker",
|
"--worker",
|
||||||
|
"--backend",
|
||||||
|
args.backend,
|
||||||
"--model",
|
"--model",
|
||||||
args.model,
|
args.model,
|
||||||
"--samplerate",
|
"--samplerate",
|
||||||
@@ -364,16 +487,32 @@ def start_background(args: argparse.Namespace) -> int:
|
|||||||
str(args.notify_interval),
|
str(args.notify_interval),
|
||||||
"--state-dir",
|
"--state-dir",
|
||||||
str(state_dir),
|
str(state_dir),
|
||||||
|
"--device",
|
||||||
|
args.device,
|
||||||
|
"--compute-type",
|
||||||
|
args.compute_type,
|
||||||
|
"--beam-size",
|
||||||
|
str(args.beam_size),
|
||||||
]
|
]
|
||||||
if args.whisper_bin:
|
|
||||||
cmd.extend(["--whisper-bin", args.whisper_bin])
|
|
||||||
|
|
||||||
|
log_path = state_dir / "worker.log"
|
||||||
|
with log_path.open("a", encoding="utf-8") as log_fh:
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=log_fh,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=log_fh,
|
||||||
start_new_session=True,
|
start_new_session=True,
|
||||||
)
|
)
|
||||||
|
_append_log(state_dir, "start requested")
|
||||||
|
# If worker fails immediately (common with model/device config issues),
|
||||||
|
# surface that early instead of only showing "No active recording" later.
|
||||||
|
time.sleep(0.15)
|
||||||
|
worker_error = _read_and_clear_error(state_dir / "error.txt")
|
||||||
|
if worker_error:
|
||||||
|
print(worker_error, file=sys.stderr)
|
||||||
|
Notifier().send("Transcription error", worker_error, timeout_ms=3000)
|
||||||
|
return 1
|
||||||
|
|
||||||
Notifier().send(
|
Notifier().send(
|
||||||
"Recording", "Started (press keybind again to stop)", timeout_ms=1200
|
"Recording", "Started (press keybind again to stop)", timeout_ms=1200
|
||||||
)
|
)
|
||||||
@@ -389,9 +528,14 @@ def stop_background(args: argparse.Namespace) -> int:
|
|||||||
pid = _read_pid(pid_file)
|
pid = _read_pid(pid_file)
|
||||||
|
|
||||||
if not _is_alive(pid):
|
if not _is_alive(pid):
|
||||||
|
worker_error = _read_and_clear_error(error_file)
|
||||||
|
if worker_error:
|
||||||
|
print(worker_error, file=sys.stderr)
|
||||||
|
Notifier().send("Transcription error", worker_error, timeout_ms=3000)
|
||||||
|
return 1
|
||||||
if pid_file.exists():
|
if pid_file.exists():
|
||||||
pid_file.unlink()
|
pid_file.unlink()
|
||||||
print("No active recording.")
|
print(f"No active recording. Check log: {state_dir / 'worker.log'}")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
assert pid is not None
|
assert pid is not None
|
||||||
@@ -404,12 +548,13 @@ def stop_background(args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
if _is_alive(pid):
|
if _is_alive(pid):
|
||||||
print("Timed out waiting for transcription to finish.", file=sys.stderr)
|
print("Timed out waiting for transcription to finish.", file=sys.stderr)
|
||||||
|
_append_log(state_dir, "stop timeout waiting for worker exit")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if error_file.exists():
|
worker_error = _read_and_clear_error(error_file)
|
||||||
message = error_file.read_text(encoding="utf-8").strip()
|
if worker_error:
|
||||||
error_file.unlink()
|
print(worker_error, file=sys.stderr)
|
||||||
print(message or "Worker failed.", file=sys.stderr)
|
Notifier().send("Transcription error", worker_error, timeout_ms=3000)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
text = ""
|
text = ""
|
||||||
@@ -427,7 +572,7 @@ def stop_background(args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Record from microphone and transcribe with whisper.cpp"
|
description="Record from microphone and transcribe with whisper.cpp or faster-whisper"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mode",
|
"--mode",
|
||||||
@@ -435,18 +580,24 @@ def parse_args() -> argparse.Namespace:
|
|||||||
default="once",
|
default="once",
|
||||||
help="once: record/transcribe immediately, start/stop: background toggle pieces, toggle: start if idle else stop",
|
help="once: record/transcribe immediately, start/stop: background toggle pieces, toggle: start if idle else stop",
|
||||||
)
|
)
|
||||||
|
parser.add_argument("--start", action="store_true", help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument("--stop", action="store_true", help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument("--toggle", action="store_true", help=argparse.SUPPRESS)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--worker",
|
"--worker",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help=argparse.SUPPRESS,
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--model", default=DEFAULT_MODEL, help="Path to whisper.cpp model"
|
"--backend",
|
||||||
|
choices=("whispercpp", "ctranslate2"),
|
||||||
|
default="whispercpp",
|
||||||
|
help="Transcription backend (default: whispercpp)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--whisper-bin",
|
"--model",
|
||||||
default=None,
|
default=DEFAULT_MODEL,
|
||||||
help="Path to whisper.cpp binary (default: auto-detect whisper-cli/main)",
|
help="Model name or path. For whispercpp: ggml .bin path/name. For ctranslate2: model name or model directory.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--duration",
|
"--duration",
|
||||||
@@ -475,6 +626,22 @@ def parse_args() -> argparse.Namespace:
|
|||||||
default="print",
|
default="print",
|
||||||
help="How to emit transcript text: print to terminal or type into active window",
|
help="How to emit transcript text: print to terminal or type into active window",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--device",
|
||||||
|
default="auto",
|
||||||
|
help="Inference device for faster-whisper (auto, cpu, cuda)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--compute-type",
|
||||||
|
default="auto",
|
||||||
|
help="faster-whisper compute type (auto, default, float16, int8, int8_float16, ...)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--beam-size",
|
||||||
|
type=int,
|
||||||
|
default=5,
|
||||||
|
help="Beam size for decoding (default: 5)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--state-dir",
|
"--state-dir",
|
||||||
default=str(DEFAULT_STATE_DIR),
|
default=str(DEFAULT_STATE_DIR),
|
||||||
@@ -486,7 +653,27 @@ def parse_args() -> argparse.Namespace:
|
|||||||
default=90.0,
|
default=90.0,
|
||||||
help="Max seconds to wait for background transcription to finish on stop",
|
help="Max seconds to wait for background transcription to finish on stop",
|
||||||
)
|
)
|
||||||
return parser.parse_args()
|
parser.add_argument(
|
||||||
|
"--toggle-debounce",
|
||||||
|
type=float,
|
||||||
|
default=DEFAULT_TOGGLE_DEBOUNCE,
|
||||||
|
help="Ignore repeated toggle triggers within this many seconds (default: 0.0, disabled)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
legacy_modes = [
|
||||||
|
mode
|
||||||
|
for flag, mode in (
|
||||||
|
(args.start, "start"),
|
||||||
|
(args.stop, "stop"),
|
||||||
|
(args.toggle, "toggle"),
|
||||||
|
)
|
||||||
|
if flag
|
||||||
|
]
|
||||||
|
if len(legacy_modes) > 1:
|
||||||
|
parser.error("Use only one of --start, --stop, or --toggle.")
|
||||||
|
if legacy_modes:
|
||||||
|
args.mode = legacy_modes[0]
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -505,6 +692,22 @@ def main() -> int:
|
|||||||
return stop_background(args)
|
return stop_background(args)
|
||||||
|
|
||||||
state_dir = Path(args.state_dir)
|
state_dir = Path(args.state_dir)
|
||||||
|
state_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
debounce_file = state_dir / "last-toggle.txt"
|
||||||
|
now = time.monotonic()
|
||||||
|
if args.toggle_debounce > 0 and debounce_file.exists():
|
||||||
|
try:
|
||||||
|
last = float(debounce_file.read_text(encoding="utf-8").strip())
|
||||||
|
except ValueError:
|
||||||
|
last = 0.0
|
||||||
|
if now - last < args.toggle_debounce:
|
||||||
|
_append_log(
|
||||||
|
state_dir,
|
||||||
|
f"toggle ignored by debounce: delta={now - last:.3f}s < {args.toggle_debounce:.3f}s",
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
debounce_file.write_text(f"{now:.6f}", encoding="utf-8")
|
||||||
pid = _read_pid(state_dir / "recording.pid")
|
pid = _read_pid(state_dir / "recording.pid")
|
||||||
if _is_alive(pid):
|
if _is_alive(pid):
|
||||||
return stop_background(args)
|
return stop_background(args)
|
||||||
@@ -512,4 +715,21 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
except Exception as exc:
|
||||||
|
state_dir = DEFAULT_STATE_DIR
|
||||||
|
try:
|
||||||
|
argv = sys.argv[1:]
|
||||||
|
if "--state-dir" in argv:
|
||||||
|
idx = argv.index("--state-dir")
|
||||||
|
if idx + 1 < len(argv):
|
||||||
|
state_dir = Path(argv[idx + 1]).expanduser()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_append_log(
|
||||||
|
state_dir,
|
||||||
|
"fatal exception: "
|
||||||
|
+ "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)).strip(),
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user