mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-04-09 16:19:23 -07:00
Compare commits
11 Commits
d4cd0d51c7
...
17a8347a3a
| Author | SHA1 | Date | |
|---|---|---|---|
|
17a8347a3a
|
|||
|
2cfda7d222
|
|||
|
09e4038309
|
|||
|
8bab55f793
|
|||
|
b707558d36
|
|||
|
4bb15f7f29
|
|||
|
781262a881
|
|||
|
8e171bf47f
|
|||
|
0e369ee61d
|
|||
|
494226f524
|
|||
|
1d8e65d4e2
|
@@ -16,7 +16,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
|
||||
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
"enabledPlugins": {
|
||||
"pyright-lsp@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"clangd-lsp@claude-plugins-official": true
|
||||
"clangd-lsp@claude-plugins-official": true,
|
||||
"claude-mem@thedotmack": true
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": false,
|
||||
|
||||
@@ -4,9 +4,22 @@
|
||||
"pr": ""
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm run lint)",
|
||||
"Bash(npm run test *)",
|
||||
"Read(~/.zshrc)",
|
||||
"Bash(git * main)",
|
||||
"Bash(ls *)",
|
||||
"Bash(pnpm build *)"
|
||||
],
|
||||
"deny": [
|
||||
"Read(.env)",
|
||||
"Read(~/.aws/**)"
|
||||
"Bash(curl *)",
|
||||
"Read(./.env)",
|
||||
"Read(./.env.*)",
|
||||
"Read(./secrets/**)",
|
||||
"Read(~/.aws/**)",
|
||||
"Bash(git push *)",
|
||||
"Bash(yadm push *)"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
@@ -25,19 +38,20 @@
|
||||
"enabledPlugins": {
|
||||
"pyright-lsp@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"clangd-lsp@claude-plugins-official": true
|
||||
"clangd-lsp@claude-plugins-official": true,
|
||||
"claude-mem@thedotmack": true,
|
||||
"frontend-design@claude-plugins-official": true,
|
||||
"code-review@claude-plugins-official": true,
|
||||
"code-simplifier@claude-plugins-official": true,
|
||||
"playwright@claude-plugins-official": true
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": false,
|
||||
"autoAllowBashIfSandboxed": true,
|
||||
"network": {
|
||||
"allowUnixSockets": [
|
||||
"/var/run/docker.sock"
|
||||
],
|
||||
"allowUnixSockets": ["/var/run/docker.sock"],
|
||||
"allowLocalBinding": true
|
||||
},
|
||||
"excludedCommands": [
|
||||
"docker"
|
||||
]
|
||||
"excludedCommands": ["docker"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
|
||||
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
|
||||
color_theme = "TTY"
|
||||
color_theme = "/home/sudacode/.config/btop/themes/catppuccin_macchiato.theme"
|
||||
|
||||
#* If the theme set background should be shown, set to False if you want terminal background transparency.
|
||||
theme_background = true
|
||||
|
||||
@@ -15,7 +15,7 @@ confirm-close-surface = false
|
||||
copy-on-select = clipboard
|
||||
app-notifications = no-clipboard-copy
|
||||
keybind = all:ctrl+enter=unbind
|
||||
keybind = all:ctrl+grave_accent=toggle_quick_terminal
|
||||
keybind = all:super+grave_accent=toggle_quick_terminal
|
||||
shell-integration = zsh
|
||||
keybind = shift+enter=text:\x1b\r
|
||||
shell-integration-features = title,sudo,ssh-env,ssh-terminfo
|
||||
|
||||
@@ -271,3 +271,10 @@ debug {
|
||||
disable_logs = true
|
||||
enable_stdout_logs = false
|
||||
}
|
||||
|
||||
|
||||
layerrule {
|
||||
name = fix-rofi
|
||||
match:namespace = rofi
|
||||
no_anim = true
|
||||
}
|
||||
|
||||
@@ -152,4 +152,3 @@ bind = $mainMod, a, exec, ~/.config/rofi/scripts/rofi-anki-script.sh
|
||||
bindl = , mouse:275, exec, xdotool key alt+w # top mouse to texthooker
|
||||
bindl = , mouse:276, exec, xdotool key alt+grave # bottom mouse to overlay
|
||||
bind = ALT, g, exec, /opt/mpv-yomitan/mpv-yomitan.AppImage --toggle
|
||||
# bind = ALT SHIFT, Y, exec, "$HOME/.config/rofi/scripts/rofi-mpv-yomitan.sh"
|
||||
|
||||
Submodule .config/mpv-modules/ModernZ updated: fadaf74b08...d917f6e253
BIN
.config/mpv/fonts/NotoSansCJKjp-Regular.otf
Normal file
BIN
.config/mpv/fonts/NotoSansCJKjp-Regular.otf
Normal file
Binary file not shown.
@@ -37,6 +37,8 @@ audio-wait-open=0.1 # Shorten audio device warm-up for snappier playback
|
||||
# --- Networking & remote sources ---
|
||||
ytdl-format=bestvideo+bestaudio/best
|
||||
ytdl-raw-options=sub-langs=en.*,write-auto-subs=
|
||||
ytdl-raw-options-append=extractor-args=youtubepot-bgutilhttp:base_url=http://tubearchivist:4416
|
||||
|
||||
# --- Video output & decoding ---
|
||||
vo=gpu-next
|
||||
hwdec=nvdec
|
||||
@@ -148,10 +150,12 @@ keepaspect=no
|
||||
|
||||
[immersion]
|
||||
cookies=yes
|
||||
cookies-file=/truenas/sudacode/japanese/cookies.Japanese.txt
|
||||
ytdl-raw-options=mark-watched=,write-auto-subs=,sub-langs=ja.*
|
||||
ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/cookies.Japanese.txt
|
||||
cookies-file=/truenas/sudacode/japanese/youtube-cookies.txt
|
||||
ytdl-format=bestvideo+bestaudio/best
|
||||
ytdl-raw-options=mark-watched=
|
||||
ytdl-raw-options-append=write-auto-subs=
|
||||
ytdl-raw-options-append=sub-langs=ja.*|en|ja-en
|
||||
ytdl-raw-options-append=cookies=/truenas/sudacode/japanese/youtube-cookies.txt
|
||||
sub-auto=fuzzy
|
||||
alang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
||||
slang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
||||
@@ -162,7 +166,7 @@ glsl-shaders="~~/shaders/ArtCNN_C4F32.glsl"
|
||||
scale=ewa_lanczossharp
|
||||
dither=error-diffusion
|
||||
deband=yes # Crucial for anime gradients
|
||||
input-ipc-server=/tmp/mpv-yomitan-socket
|
||||
input-ipc-server=/tmp/subminer-socket
|
||||
|
||||
[anime-subs]
|
||||
profile-cond=p["slang"] == "ja" or p["slang"] == "ja.hi"
|
||||
|
||||
@@ -31,6 +31,7 @@ sub-pos=90
|
||||
# Networking & streaming
|
||||
ytdl-format=bestvideo+bestaudio/best
|
||||
ytdl-raw-options=sub-langs=en.*,write-auto-subs=
|
||||
ytdl-raw-options-append=extractor-args=youtubepot-bgutilhttp:base_url=http://tubearchivist:4416
|
||||
# Stats & UI colors (Catppuccin Macchiato)
|
||||
background-color='#24273a'
|
||||
osd-back-color='#181926'
|
||||
@@ -179,13 +180,16 @@ keepaspect=no
|
||||
# Japanese immersion profile
|
||||
[immersion]
|
||||
cookies=yes
|
||||
cookies-file=/Volumes/sudacode/japanese/cookies.Japanese.txt
|
||||
ytdl-raw-options=mark-watched=,write-auto-subs=,sub-langs=ja.*
|
||||
ytdl-raw-options-append=cookies=/Volumes/sudacode/japanese/cookies.Japanese.txt
|
||||
cookies-file=/Volumes/sudacode/japanese/youtube-cookies.txt
|
||||
ytdl-raw-options=mark-watched=
|
||||
ytdl-raw-options-append=write-auto-subs=
|
||||
ytdl-raw-options-append=sub-langs=ja.*|en|ja-en
|
||||
ytdl-raw-options-append=cookies=/Volumes/sudacode/japanese/youtube-cookies.txt
|
||||
ytdl-format=bestvideo+bestaudio/best
|
||||
sub-auto=fuzzy
|
||||
alang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
||||
slang=ja,jp,jpn,japanese,en,eng,english,English,enUS,en-US
|
||||
tlang=en,eng,english,English,enUS,en-US
|
||||
vlang=ja,jpn
|
||||
subs-with-matching-audio=yes
|
||||
sub-font="Noto Sans CJK JP Regular"
|
||||
@@ -193,7 +197,7 @@ glsl-shaders="~~/shaders/ArtCNN_C4F32.glsl"
|
||||
scale=ewa_lanczossharp
|
||||
dither=error-diffusion
|
||||
deband=yes # Crucial for anime gradients
|
||||
input-ipc-server=/tmp/mpv-yomitan-socket
|
||||
input-ipc-server=/tmp/subminer-socket
|
||||
|
||||
# Anime subtitles profile
|
||||
[anime-subs]
|
||||
|
||||
@@ -30,6 +30,6 @@ menu_timeout=5
|
||||
show_errors=yes
|
||||
ytdlp_file_format=mp4
|
||||
ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s
|
||||
use_history_db=yes
|
||||
use_history_db=no
|
||||
backend_host=http://localhost
|
||||
backend_port=42069
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../submodules/animecards/animecards
|
||||
@@ -1 +0,0 @@
|
||||
../submodules/autosubsync-mpv
|
||||
@@ -1,388 +0,0 @@
|
||||
// Go to https://jimaku.cc/login and create a new account.
|
||||
// Then go to https://jimaku.cc/account and click the `Generate` button to create a new API key
|
||||
// Click the `Copy` button and paste it below
|
||||
var API_KEY = "";
|
||||
|
||||
// Configuration options
|
||||
var CONFIG = {
|
||||
// Filter the response to only have the specified episode
|
||||
prompt_episode: true,
|
||||
|
||||
// Subtitle suffix (e.g., ".JA" for Japanese subtitles)
|
||||
subtitle_suffix: ".JA",
|
||||
|
||||
// Preferred subtitle format (order matters, first is most preferred)
|
||||
preferred_formats: ["ass", "srt", "vtt"],
|
||||
|
||||
// Automatically load the subtitle after download
|
||||
auto_load: true,
|
||||
|
||||
// Default subtitle delay in seconds (can be positive or negative)
|
||||
default_delay: 0,
|
||||
|
||||
// Default subtitle font size
|
||||
default_font_size: 16,
|
||||
|
||||
// Automatically rename the subtitle file after download
|
||||
auto_rename: true,
|
||||
|
||||
// Automatically run autosubsync-mpv after downloading the subtitle
|
||||
run_auto_subsync: true
|
||||
};
|
||||
|
||||
// Keybindings
|
||||
// var MANUAL_SEARCH_KEY = "g";
|
||||
var FILENAME_AUTO_SEARCH_KEY = "ctrl+J";
|
||||
var PARENT_FOLDER_AUTO_SEARCH_KEY = "n";
|
||||
|
||||
function api(url, extraArgs) {
|
||||
var baseArgs = [
|
||||
"curl",
|
||||
"-s",
|
||||
"--url",
|
||||
url,
|
||||
"--header",
|
||||
"Authorization: " + API_KEY
|
||||
];
|
||||
|
||||
var args = Array.prototype.concat.apply(baseArgs, extraArgs);
|
||||
|
||||
var res = mp.command_native({
|
||||
name: "subprocess",
|
||||
playback_only: false,
|
||||
capture_stdout: true,
|
||||
capture_stderr: true,
|
||||
args: args
|
||||
});
|
||||
|
||||
if (res.stdout) return JSON.parse(res.stdout);
|
||||
}
|
||||
|
||||
function downloadSub(sub) {
|
||||
return api(sub.url, ["--output", sub.name]);
|
||||
}
|
||||
|
||||
function showMessage(message, persist) {
|
||||
var ass_start = mp.get_property_osd("osd-ass-cc/0");
|
||||
var ass_stop = mp.get_property_osd("osd-ass-cc/1");
|
||||
|
||||
mp.osd_message(
|
||||
ass_start + "{\\fs16}" + message + ass_stop,
|
||||
persist ? 999 : 2
|
||||
);
|
||||
}
|
||||
|
||||
// The timeout is neccessary due to a weird bug in mpv
|
||||
function inputGet(args) {
|
||||
mp.input.terminate();
|
||||
setTimeout(function () {
|
||||
mp.input.get(args);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// The timeout is neccessary due to a weird bug in mpv
|
||||
function inputSelect(args) {
|
||||
mp.input.terminate();
|
||||
setTimeout(function () {
|
||||
mp.input.select(args);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// Taken from mpv-subversive
|
||||
// https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L146
|
||||
function sanitize(text) {
|
||||
var subPatterns = [
|
||||
/\.[a-zA-Z]+$/, // extension
|
||||
/\./g,
|
||||
/-/g,
|
||||
/_/g,
|
||||
/\[[^\]]+\]/g, // [] bracket
|
||||
/\([^\)]+\)/g, // () bracket
|
||||
/720[pP]/g,
|
||||
/480[pP]/g,
|
||||
/1080[pP]/g,
|
||||
/[xX]26[45]/g,
|
||||
/[bB]lu[-]?[rR]ay/g,
|
||||
/^[\s]*/,
|
||||
/[\s]*$/,
|
||||
/1920x1080/g,
|
||||
/1920X1080/g,
|
||||
/Hi10P/g,
|
||||
/FLAC/g,
|
||||
/AAC/g
|
||||
];
|
||||
|
||||
var result = text;
|
||||
|
||||
subPatterns.forEach(function (subPattern) {
|
||||
var newResult = result.replace(subPattern, " ");
|
||||
if (newResult.length > 0) {
|
||||
result = newResult;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Adapted from mpv-subversive
|
||||
// https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L164
|
||||
function extractTitle(text) {
|
||||
var matchers = [
|
||||
{ regex: /^([\w\s\d]+)[Ss]\d+[Ee]?\d+/, group: 1 },
|
||||
{ regex: /^([\w\s\d]+)-[\s]*\d+[\s]*[^\w]*$/, group: 1 },
|
||||
{ regex: /^([\w\s\d]+)[Ee]?[Pp]?[\s]+\d+$/, group: 1 },
|
||||
{ regex: /^([\w\s\d]+)[\s]\d+.*$/, group: 1 },
|
||||
{ regex: /^\d+[\s]*(.+)$/, group: 1 }
|
||||
];
|
||||
|
||||
for (var i = 0; i < matchers.length; i++) {
|
||||
var matcher = matchers[i];
|
||||
var match = text.match(matcher.regex);
|
||||
if (match) {
|
||||
return match[matcher.group].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function getNames(results) {
|
||||
return results.map(function (item) {
|
||||
return item.name;
|
||||
});
|
||||
}
|
||||
|
||||
function runAutoSubSyncMPV() {
|
||||
try {
|
||||
mp.command_native(["script-binding", "autosubsync-menu"]);
|
||||
} catch (e) {
|
||||
showMessage("autosubsync-mpv not installed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function selectSub(selectedSub) {
|
||||
showMessage("Downloading: " + selectedSub.name);
|
||||
|
||||
try {
|
||||
downloadSub(selectedSub);
|
||||
|
||||
// Get current video filename without extension
|
||||
var videoPath = mp.get_property("path");
|
||||
if (!videoPath) {
|
||||
throw new Error("No video file is currently playing");
|
||||
}
|
||||
var videoName = videoPath.substring(0, videoPath.lastIndexOf("."));
|
||||
|
||||
// Get subtitle extension
|
||||
var subExt = selectedSub.name.substring(selectedSub.name.lastIndexOf("."));
|
||||
|
||||
var newSubName = selectedSub.name;
|
||||
if (CONFIG.auto_rename) {
|
||||
// Create new subtitle filename
|
||||
newSubName = videoName + CONFIG.subtitle_suffix + subExt;
|
||||
|
||||
// Rename the downloaded subtitle file
|
||||
var renameResult = mp.command_native({
|
||||
name: "subprocess",
|
||||
playback_only: false,
|
||||
args: ["mv", selectedSub.name, newSubName]
|
||||
});
|
||||
|
||||
if (renameResult.error) {
|
||||
throw new Error(
|
||||
"Failed to rename subtitle file: " + renameResult.error
|
||||
);
|
||||
}
|
||||
|
||||
showMessage(newSubName + " downloaded and renamed");
|
||||
} else {
|
||||
showMessage(newSubName + " downloaded");
|
||||
}
|
||||
|
||||
if (CONFIG.auto_load) {
|
||||
mp.commandv("sub_add", newSubName);
|
||||
showMessage(newSubName + " added");
|
||||
|
||||
// Apply subtitle settings if configured
|
||||
if (CONFIG.default_delay !== 0) {
|
||||
mp.commandv("sub_delay", CONFIG.default_delay);
|
||||
}
|
||||
if (CONFIG.default_font_size !== 16) {
|
||||
mp.commandv("sub_font_size", CONFIG.default_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (CONFIG.run_auto_subsync) {
|
||||
runAutoSubSyncMPV();
|
||||
}
|
||||
|
||||
mp.set_property("pause", "no");
|
||||
} catch (error) {
|
||||
showMessage("Error: " + error.message, true);
|
||||
mp.set_property("pause", "no");
|
||||
}
|
||||
}
|
||||
|
||||
function sortByPreferredFormat(files) {
|
||||
return files.sort(function (a, b) {
|
||||
var extA = a.name.substring(a.name.lastIndexOf(".") + 1).toLowerCase();
|
||||
var extB = b.name.substring(b.name.lastIndexOf(".") + 1).toLowerCase();
|
||||
|
||||
var indexA = CONFIG.preferred_formats.indexOf(extA);
|
||||
var indexB = CONFIG.preferred_formats.indexOf(extB);
|
||||
|
||||
if (indexA === -1) return 1;
|
||||
if (indexB === -1) return -1;
|
||||
return indexA - indexB;
|
||||
});
|
||||
}
|
||||
|
||||
function selectEpisode(anime, episode) {
|
||||
mp.input.terminate();
|
||||
var episodeResults;
|
||||
|
||||
if (episode) {
|
||||
showMessage("Fetching subs for: " + anime.name + " episode " + episode);
|
||||
episodeResults = api(
|
||||
"https://jimaku.cc/api/entries/" + anime.id + "/files?episode=" + episode
|
||||
);
|
||||
} else {
|
||||
showMessage("Fetching all subs for: " + anime.name);
|
||||
episodeResults = api(
|
||||
"https://jimaku.cc/api/entries/" + anime.id + "/files"
|
||||
);
|
||||
}
|
||||
|
||||
if (episodeResults.error) {
|
||||
showMessage("Error: " + animeResults.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (episodeResults.length === 0) {
|
||||
showMessage("No results found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort results by preferred format
|
||||
episodeResults = sortByPreferredFormat(episodeResults);
|
||||
|
||||
if (episodeResults.length === 1) {
|
||||
var selectedEpisode = episodeResults[0];
|
||||
selectSub(selectedEpisode);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = getNames(episodeResults);
|
||||
|
||||
inputSelect({
|
||||
prompt: "Select episode: ",
|
||||
items: items,
|
||||
submit: function (id) {
|
||||
var selectedEpisode = episodeResults[id - 1];
|
||||
selectSub(selectedEpisode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onAnimeSelected(anime) {
|
||||
if (CONFIG.prompt_episode) {
|
||||
inputGet({
|
||||
prompt: "Episode (leave blank for all): ",
|
||||
submit: function (episode) {
|
||||
selectEpisode(anime, episode);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectEpisode(anime);
|
||||
}
|
||||
}
|
||||
|
||||
function search(searchTerm, isAuto) {
|
||||
mp.input.terminate();
|
||||
showMessage('Searching for: "' + searchTerm + '"');
|
||||
|
||||
var animeResults = api(
|
||||
encodeURI(
|
||||
"https://jimaku.cc/api/entries/search?anime=true&query=" + searchTerm
|
||||
)
|
||||
);
|
||||
|
||||
if (animeResults.error) {
|
||||
showMessage("Error: " + animeResults.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (animeResults.length === 0) {
|
||||
showMessage("No results found");
|
||||
if (isAuto) {
|
||||
manualSearch(searchTerm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (animeResults.length === 1) {
|
||||
var selectedAnime = animeResults[0];
|
||||
onAnimeSelected(selectedAnime);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = getNames(animeResults);
|
||||
|
||||
inputSelect({
|
||||
prompt: "Select anime: ",
|
||||
items: items,
|
||||
submit: function (id) {
|
||||
var selectedAnime = animeResults[id - 1];
|
||||
showMessage(selectedAnime.name, true);
|
||||
onAnimeSelected(selectedAnime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function manualSearch(defaultText) {
|
||||
inputGet({
|
||||
prompt: "Search term: ",
|
||||
submit: search,
|
||||
default_text: defaultText
|
||||
});
|
||||
|
||||
mp.set_property("pause", "yes");
|
||||
showMessage("Manual Jimaku Search", true);
|
||||
}
|
||||
|
||||
function autoSearch() {
|
||||
var filename = mp.get_property("filename");
|
||||
var sanitizedFilename = sanitize(filename);
|
||||
var currentAnime = extractTitle(sanitizedFilename);
|
||||
|
||||
mp.set_property("pause", "yes");
|
||||
|
||||
search(currentAnime, true);
|
||||
}
|
||||
|
||||
function autoSearchParentFolder() {
|
||||
var path = mp.get_property("stream-open-filename");
|
||||
var pathSplit = path.split(path.indexOf("/") >= 0 ? "/" : "\\");
|
||||
var filename =
|
||||
pathSplit.length === 1 ? pathSplit[0] : pathSplit[pathSplit.length - 2];
|
||||
|
||||
var sanitizedFilename = sanitize(filename);
|
||||
var currentAnime = extractTitle(sanitizedFilename);
|
||||
|
||||
mp.set_property("pause", "yes");
|
||||
|
||||
search(currentAnime, true);
|
||||
}
|
||||
|
||||
// mp.add_key_binding(MANUAL_SEARCH_KEY, "jimaku-manual-search", manualSearch);
|
||||
mp.add_key_binding(
|
||||
FILENAME_AUTO_SEARCH_KEY,
|
||||
"jimaku-filename-auto-search",
|
||||
autoSearch
|
||||
);
|
||||
mp.add_key_binding(
|
||||
PARENT_FOLDER_AUTO_SEARCH_KEY,
|
||||
"jimaku-parent-folder-auto-search",
|
||||
autoSearchParentFolder
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
../submodules/mpvacious
|
||||
@@ -1 +0,0 @@
|
||||
../submodules/mpv-youtube-upnext/youtube-upnext.lua
|
||||
@@ -28,7 +28,7 @@ return {
|
||||
-- default = "claude-3.7-sonnet-thought",
|
||||
-- default = "o3-mini",
|
||||
-- default = "gemini-2.0-flash-001",
|
||||
default = "claude-haiku-4.5",
|
||||
default = "claude-opus-4.6",
|
||||
-- default = "gpt-4o",
|
||||
-- default = "o3-mini-2025-01-31",
|
||||
-- choices = {
|
||||
|
||||
@@ -37,7 +37,8 @@ window {
|
||||
location: center;
|
||||
anchor: center;
|
||||
fullscreen: false;
|
||||
width: 37%;
|
||||
width: 46.65%;
|
||||
height: 44%;
|
||||
x-offset: 0px;
|
||||
y-offset: 0px;
|
||||
|
||||
@@ -58,9 +59,9 @@ mainbox {
|
||||
}
|
||||
|
||||
imagebox {
|
||||
padding: 20px;
|
||||
padding: 24px;
|
||||
background-color: transparent;
|
||||
background-image: url("~/.config/rofi/images/oshinoko.png", height);
|
||||
background-image: url("~/.config/rofi/images/oshinoko.png", both);
|
||||
orientation: vertical;
|
||||
children: [ "inputbar", "dummy", "mode-switcher" ];
|
||||
}
|
||||
|
||||
203
.config/subminer/config.jsonc
Normal file → Executable file
203
.config/subminer/config.jsonc
Normal file → Executable file
@@ -1,112 +1,97 @@
|
||||
{
|
||||
"subtitlePosition": {
|
||||
"yPercent": 17.38459152016546
|
||||
},
|
||||
"keybindings": [
|
||||
{
|
||||
"key": "Space",
|
||||
"command": [
|
||||
"cycle",
|
||||
"pause"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ArrowRight",
|
||||
"command": [
|
||||
"seek",
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ArrowLeft",
|
||||
"command": [
|
||||
"seek",
|
||||
-5
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ArrowRight",
|
||||
"command": [
|
||||
"seek",
|
||||
5
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ArrowUp",
|
||||
"command": [
|
||||
"seek",
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ArrowDown",
|
||||
"command": [
|
||||
"seek",
|
||||
-60
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "KeyQ",
|
||||
"command": [
|
||||
"quit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "Ctrl+KeyW",
|
||||
"command": [
|
||||
"quit"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keybindings": [],
|
||||
"auto_start_overlay": false,
|
||||
"texthooker": {
|
||||
"openBrowser": false
|
||||
"openBrowser": false,
|
||||
},
|
||||
"websocket": {
|
||||
"enabled": "auto",
|
||||
"port": 6677
|
||||
"port": 6677,
|
||||
},
|
||||
"ankiConnect": {
|
||||
"enabled": true,
|
||||
"url": "http://127.0.0.1:8765",
|
||||
"deck": "Minecraft",
|
||||
"pollingRate": 200,
|
||||
"audioField": "ExpressionAudio",
|
||||
"imageField": "Picture",
|
||||
"sentenceField": "Sentence",
|
||||
"generateAudio": true,
|
||||
"generateImage": true,
|
||||
"imageType": "avif",
|
||||
"imageFormat": "webp",
|
||||
"miscInfoPattern": "[mpv-yomitan] %f (%t)",
|
||||
"overwriteAudio": false,
|
||||
"overwriteImage": true,
|
||||
"highlightWord": true,
|
||||
"showNotificationOnUpdate": true,
|
||||
"notificationType": "system",
|
||||
"audioPadding": 0.5,
|
||||
"fallbackDuration": 3,
|
||||
"animatedFps": 24,
|
||||
"animatedMaxWidth": 640,
|
||||
"animatedMaxHeight": null,
|
||||
"animatedCrf": 35,
|
||||
"autoUpdateNewCards": false,
|
||||
"sentenceCardModel": "Lapis Morph",
|
||||
"sentenceCardSentenceField": "Sentence",
|
||||
"sentenceCardAudioField": "SentenceAudio",
|
||||
"isLapis": true,
|
||||
"mediaInsertMode": "append",
|
||||
"auto_start_overlay": false,
|
||||
"secondarySub": {
|
||||
"autoLoadSecondarySub": true,
|
||||
"secondarySubLanguages": [
|
||||
"en",
|
||||
"eng"
|
||||
]
|
||||
}
|
||||
"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": {
|
||||
"ontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
|
||||
"fontSize": 35,
|
||||
"fontFamily": "Noto Sans CJK JP Regular, Noto Sans CJK JP, Arial Unicode MS, Arial, sans-serif",
|
||||
"fontSize": 24,
|
||||
"fontColor": "#cad3f5",
|
||||
"fontWeight": "normal",
|
||||
"fontStyle": "normal",
|
||||
@@ -114,16 +99,28 @@
|
||||
"secondary": {
|
||||
"fontSize": 24,
|
||||
"fontColor": "#cad3f5",
|
||||
"backgroundColor": "transparent"
|
||||
}
|
||||
"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",
|
||||
"multiCopyTimeoutMs": 3000
|
||||
}
|
||||
}
|
||||
"markAudioCard": "CommandOrControl+Shift+A",
|
||||
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
--embed-thumbnail
|
||||
--embed-subs
|
||||
|
||||
|
||||
# Always extract audio
|
||||
# -x
|
||||
|
||||
@@ -23,3 +22,5 @@
|
||||
--sponsorblock-chapter-title "[SponsorBlock] %(category_names)l"
|
||||
--sponsorblock-api https://sponsor.ajay.app
|
||||
--sponsorblock-chapter-title all
|
||||
|
||||
--extractor-args "youtubepot-bgutilhttp:base_url=http://tubearchivist:4416"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = '1.2.2'
|
||||
|
||||
import abc
|
||||
import json
|
||||
|
||||
from yt_dlp.extractor.youtube.pot.provider import (
|
||||
ExternalRequestFeature,
|
||||
PoTokenContext,
|
||||
PoTokenProvider,
|
||||
PoTokenProviderRejectedRequest,
|
||||
)
|
||||
from yt_dlp.extractor.youtube.pot.utils import WEBPO_CLIENTS
|
||||
from yt_dlp.utils import js_to_json
|
||||
from yt_dlp.utils.traversal import traverse_obj
|
||||
|
||||
|
||||
class BgUtilPTPBase(PoTokenProvider, abc.ABC):
|
||||
PROVIDER_VERSION = __version__
|
||||
BUG_REPORT_LOCATION = 'https://github.com/Brainicism/bgutil-ytdlp-pot-provider/issues'
|
||||
_SUPPORTED_EXTERNAL_REQUEST_FEATURES = (
|
||||
ExternalRequestFeature.PROXY_SCHEME_HTTP,
|
||||
ExternalRequestFeature.PROXY_SCHEME_HTTPS,
|
||||
ExternalRequestFeature.PROXY_SCHEME_SOCKS4,
|
||||
ExternalRequestFeature.PROXY_SCHEME_SOCKS4A,
|
||||
ExternalRequestFeature.PROXY_SCHEME_SOCKS5,
|
||||
ExternalRequestFeature.PROXY_SCHEME_SOCKS5H,
|
||||
ExternalRequestFeature.SOURCE_ADDRESS,
|
||||
ExternalRequestFeature.DISABLE_TLS_VERIFICATION,
|
||||
)
|
||||
_SUPPORTED_CLIENTS = WEBPO_CLIENTS
|
||||
_SUPPORTED_CONTEXTS = (
|
||||
PoTokenContext.GVS,
|
||||
PoTokenContext.PLAYER,
|
||||
PoTokenContext.SUBS,
|
||||
)
|
||||
_GETPOT_TIMEOUT = 20.0
|
||||
_GET_SERVER_VSN_TIMEOUT = 5.0
|
||||
_MIN_NODE_VSN = (18, 0, 0)
|
||||
|
||||
def _info_and_raise(self, msg, raise_from=None):
|
||||
self.logger.info(msg)
|
||||
raise PoTokenProviderRejectedRequest(msg) from raise_from
|
||||
|
||||
def _warn_and_raise(self, msg, once=True, raise_from=None):
|
||||
self.logger.warning(msg, once=once)
|
||||
raise PoTokenProviderRejectedRequest(msg) from raise_from
|
||||
|
||||
def _check_version(self, got_version, *, default='unknown', name):
|
||||
def _major(version):
|
||||
return version.split('.', 1)[0]
|
||||
|
||||
if got_version != self.PROVIDER_VERSION:
|
||||
self.logger.warning(
|
||||
f'The provider plugin and the {name} are on different versions, '
|
||||
f'this may cause compatibility issues. '
|
||||
f'Please ensure they are on the same version. '
|
||||
f'Otherwise, help will NOT be provided for any issues that arise. '
|
||||
f'(plugin: {self.PROVIDER_VERSION}, {name}: {got_version or default})',
|
||||
once=True,
|
||||
)
|
||||
if not got_version or _major(got_version) != _major(self.PROVIDER_VERSION):
|
||||
self._warn_and_raise(
|
||||
f'Plugin and {name} major versions are mismatched. '
|
||||
f'Update both the plugin and the {name} to the same version to proceed.'
|
||||
)
|
||||
|
||||
def _get_attestation(self, webpage: str | None):
|
||||
if not webpage:
|
||||
return None
|
||||
raw_challenge_data = self.ie._search_regex(
|
||||
r"""(?sx)window\.ytAtR\s*=\s*(?P<raw_cd>(?P<q>['"])
|
||||
(?:
|
||||
\\.|
|
||||
(?!(?P=q)).
|
||||
)*
|
||||
(?P=q))\s*;""",
|
||||
webpage,
|
||||
'raw challenge data',
|
||||
default=None,
|
||||
group='raw_cd',
|
||||
)
|
||||
att_txt = traverse_obj(raw_challenge_data, ({js_to_json}, {json.loads}, {json.loads}, 'bgChallenge'))
|
||||
if not att_txt:
|
||||
self.logger.warning('Failed to extract initial attestation from the webpage')
|
||||
return None
|
||||
return att_txt
|
||||
|
||||
|
||||
__all__ = ['__version__']
|
||||
@@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import json
|
||||
import time
|
||||
|
||||
from yt_dlp.extractor.youtube.pot.provider import (
|
||||
PoTokenProviderError,
|
||||
PoTokenProviderRejectedRequest,
|
||||
PoTokenRequest,
|
||||
PoTokenResponse,
|
||||
register_preference,
|
||||
register_provider,
|
||||
)
|
||||
from yt_dlp.extractor.youtube.pot.utils import get_webpo_content_binding
|
||||
from yt_dlp.networking.common import Request
|
||||
from yt_dlp.networking.exceptions import HTTPError, TransportError
|
||||
|
||||
from yt_dlp_plugins.extractor.getpot_bgutil import BgUtilPTPBase
|
||||
|
||||
|
||||
@register_provider
|
||||
class BgUtilHTTPPTP(BgUtilPTPBase):
|
||||
PROVIDER_NAME = 'bgutil:http'
|
||||
DEFAULT_BASE_URL = 'http://127.0.0.1:4416'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._last_server_check = 0
|
||||
self._server_available = True
|
||||
|
||||
@functools.cached_property
|
||||
def _base_url(self):
|
||||
base_url = self._configuration_arg('base_url', default=[None])[0]
|
||||
|
||||
if base_url:
|
||||
return base_url
|
||||
|
||||
# check deprecated arg
|
||||
deprecated_base_url = self.ie._configuration_arg(
|
||||
ie_key='youtube', key='getpot_bgutil_baseurl', default=[None])[0]
|
||||
if deprecated_base_url:
|
||||
self._warn_and_raise(
|
||||
"'youtube:getpot_bgutil_baseurl' extractor arg is deprecated, use 'youtubepot-bgutilhttp:base_url' instead")
|
||||
|
||||
# default if no arg was passed
|
||||
self.logger.debug(
|
||||
f'No base_url provided, defaulting to {self.DEFAULT_BASE_URL}')
|
||||
return self.DEFAULT_BASE_URL
|
||||
|
||||
def _check_server_availability(self, ctx: PoTokenRequest):
|
||||
if self._last_server_check + 60 > time.time():
|
||||
return self._server_available
|
||||
|
||||
self._server_available = False
|
||||
try:
|
||||
self.logger.trace(
|
||||
f'Checking server availability at {self._base_url}/ping')
|
||||
response = json.load(self._request_webpage(Request(
|
||||
f'{self._base_url}/ping', extensions={'timeout': self._GET_SERVER_VSN_TIMEOUT}, proxies={'all': None}),
|
||||
note=False))
|
||||
except TransportError as e:
|
||||
# the server may be down
|
||||
script_path_provided = self.ie._configuration_arg(
|
||||
ie_key='youtubepot-bgutilscript', key='script_path', default=[None])[0] is not None
|
||||
|
||||
warning_base = f'Error reaching GET {self._base_url}/ping (caused by {e.__class__.__name__}). '
|
||||
if script_path_provided: # server down is expected, log info
|
||||
self._info_and_raise(
|
||||
warning_base + 'This is expected if you are using the script method.')
|
||||
else:
|
||||
self._warn_and_raise(
|
||||
warning_base + f'Please make sure that the server is reachable at {self._base_url}.')
|
||||
|
||||
return
|
||||
except HTTPError as e:
|
||||
# may be an old server, don't raise
|
||||
self.logger.warning(
|
||||
f'HTTP Error reaching GET /ping (caused by {e!r})', once=True)
|
||||
return
|
||||
except json.JSONDecodeError as e:
|
||||
# invalid server
|
||||
self._warn_and_raise(
|
||||
f'Error parsing ping response JSON (caused by {e!r})')
|
||||
return
|
||||
except Exception as e:
|
||||
self._warn_and_raise(
|
||||
f'Unknown error reaching GET /ping (caused by {e!r})', raise_from=e)
|
||||
return
|
||||
else:
|
||||
self._check_version(response.get('version', ''), name='HTTP server')
|
||||
self._server_available = True
|
||||
return True
|
||||
finally:
|
||||
self._last_server_check = time.time()
|
||||
|
||||
def is_available(self):
|
||||
return self._server_available or self._last_server_check + 60 < int(time.time())
|
||||
|
||||
def _real_request_pot(
|
||||
self,
|
||||
request: PoTokenRequest,
|
||||
) -> PoTokenResponse:
|
||||
if not self._check_server_availability(request):
|
||||
raise PoTokenProviderRejectedRequest(
|
||||
f'{self.PROVIDER_NAME} server is not available')
|
||||
|
||||
# used for CI check
|
||||
self.logger.trace('Generating POT via HTTP server')
|
||||
|
||||
disable_innertube = bool(self._configuration_arg('disable_innertube', default=[None])[0])
|
||||
challenge = self._get_attestation(None if disable_innertube else request.video_webpage)
|
||||
# The challenge is falsy when the webpage and the challenge are unavailable
|
||||
# In this case, we need to disable /att/get since it's broken for web_music
|
||||
if not challenge and request.internal_client_name == 'web_music':
|
||||
if not disable_innertube: # if not already set, warn the user
|
||||
self.logger.warning(
|
||||
'BotGuard challenges could not be obtained from the webpage, '
|
||||
'overriding disable_innertube=True because InnerTube challenges '
|
||||
'are currently broken for the web_music client. '
|
||||
'Pass disable_innertube=1 to suppress this warning.')
|
||||
disable_innertube = True
|
||||
|
||||
try:
|
||||
response = self._request_webpage(
|
||||
request=Request(
|
||||
f'{self._base_url}/get_pot', data=json.dumps({
|
||||
'bypass_cache': request.bypass_cache,
|
||||
'challenge': challenge,
|
||||
'content_binding': get_webpo_content_binding(request)[0],
|
||||
'disable_innertube': disable_innertube,
|
||||
'disable_tls_verification': not request.request_verify_tls,
|
||||
'proxy': request.request_proxy,
|
||||
'innertube_context': request.innertube_context,
|
||||
'source_address': request.request_source_address,
|
||||
}).encode(), headers={'Content-Type': 'application/json'},
|
||||
extensions={'timeout': self._GETPOT_TIMEOUT}, proxies={'all': None}),
|
||||
note=f'Generating a {request.context.value} PO Token for '
|
||||
f'{request.internal_client_name} client via bgutil HTTP server',
|
||||
)
|
||||
except Exception as e:
|
||||
raise PoTokenProviderError(
|
||||
f'Error reaching POST /get_pot (caused by {e!r})') from e
|
||||
|
||||
try:
|
||||
response_json = json.load(response)
|
||||
except Exception as e:
|
||||
raise PoTokenProviderError(
|
||||
f'Error parsing response JSON (caused by {e!r}). response = {response.read().decode()}') from e
|
||||
|
||||
if error_msg := response_json.get('error'):
|
||||
raise PoTokenProviderError(error_msg)
|
||||
if 'poToken' not in response_json:
|
||||
raise PoTokenProviderError(
|
||||
f'Server did not respond with a poToken. Received response: {response}')
|
||||
|
||||
po_token = response_json['poToken']
|
||||
self.logger.trace(f'Generated POT: {po_token}')
|
||||
return PoTokenResponse(po_token=po_token)
|
||||
|
||||
|
||||
@register_preference(BgUtilHTTPPTP)
|
||||
def bgutil_HTTP_getpot_preference(provider, request):
|
||||
return 130
|
||||
|
||||
|
||||
__all__ = [BgUtilHTTPPTP.__name__,
|
||||
bgutil_HTTP_getpot_preference.__name__]
|
||||
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from yt_dlp.extractor.youtube.pot.provider import (
|
||||
PoTokenProviderError,
|
||||
PoTokenRequest,
|
||||
PoTokenResponse,
|
||||
register_preference,
|
||||
register_provider,
|
||||
)
|
||||
from yt_dlp.extractor.youtube.pot.utils import get_webpo_content_binding
|
||||
from yt_dlp.utils import Popen
|
||||
|
||||
from yt_dlp_plugins.extractor.getpot_bgutil import BgUtilPTPBase
|
||||
|
||||
|
||||
@register_provider
|
||||
class BgUtilScriptPTP(BgUtilPTPBase):
|
||||
PROVIDER_NAME = 'bgutil:script'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._check_script = functools.cache(self._check_script_impl)
|
||||
|
||||
@functools.cached_property
|
||||
def _script_path(self):
|
||||
script_path = self._configuration_arg(
|
||||
'script_path', casesense=True, default=[None])[0]
|
||||
|
||||
if script_path:
|
||||
return os.path.expandvars(script_path)
|
||||
|
||||
# check deprecated arg
|
||||
deprecated_script_path = self.ie._configuration_arg(
|
||||
ie_key='youtube', key='getpot_bgutil_script', default=[None])[0]
|
||||
|
||||
if deprecated_script_path:
|
||||
self._warn_and_raise(
|
||||
"'youtube:getpot_bgutil_script' extractor arg is deprecated, use 'youtubepot-bgutilscript:script_path' instead")
|
||||
|
||||
# default if no arg was passed
|
||||
home = os.path.expanduser('~')
|
||||
default_path = os.path.join(
|
||||
home, 'bgutil-ytdlp-pot-provider', 'server', 'build', 'generate_once.js')
|
||||
self.logger.debug(
|
||||
f'No script path passed, defaulting to {default_path}')
|
||||
return default_path
|
||||
|
||||
def is_available(self):
|
||||
return self._check_script(self._script_path)
|
||||
|
||||
@functools.cached_property
|
||||
def _node_path(self):
|
||||
node_path = shutil.which('node')
|
||||
if node_path is None:
|
||||
self.logger.trace('node is not in PATH')
|
||||
vsn = self._check_node_version(node_path)
|
||||
if vsn:
|
||||
self.logger.trace(f'Node version: {vsn}')
|
||||
return node_path
|
||||
|
||||
def _check_script_impl(self, script_path):
|
||||
if not os.path.isfile(script_path):
|
||||
self.logger.debug(
|
||||
f"Script path doesn't exist: {script_path}")
|
||||
return False
|
||||
if os.path.basename(script_path) != 'generate_once.js':
|
||||
self.logger.warning(
|
||||
'Incorrect script passed to extractor args. Path to generate_once.js required', once=True)
|
||||
return False
|
||||
node_path = self._node_path
|
||||
if not node_path:
|
||||
return False
|
||||
stdout, stderr, returncode = Popen.run(
|
||||
[self._node_path, script_path, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
timeout=self._GET_SERVER_VSN_TIMEOUT)
|
||||
if returncode:
|
||||
self.logger.warning(
|
||||
f'Failed to check script version. '
|
||||
f'Script returned {returncode} exit status. '
|
||||
f'Script stdout: {stdout}; Script stderr: {stderr}',
|
||||
once=True)
|
||||
return False
|
||||
else:
|
||||
self._check_version(stdout.strip(), name='script')
|
||||
return True
|
||||
|
||||
def _check_node_version(self, node_path):
|
||||
try:
|
||||
stdout, stderr, returncode = Popen.run(
|
||||
[node_path, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
timeout=self._GET_SERVER_VSN_TIMEOUT)
|
||||
stdout = stdout.strip()
|
||||
mobj = re.match(r'v(\d+)\.(\d+)\.(\d+)', stdout)
|
||||
if returncode or not mobj:
|
||||
raise ValueError
|
||||
node_vsn = tuple(map(int, mobj.groups()))
|
||||
if node_vsn >= self._MIN_NODE_VSN:
|
||||
return node_vsn
|
||||
raise RuntimeError
|
||||
except RuntimeError:
|
||||
min_vsn_str = 'v' + '.'.join(str(v) for v in self._MIN_NODE_VSN)
|
||||
self.logger.warning(
|
||||
f'Node version too low. '
|
||||
f'(got {stdout}, but at least {min_vsn_str} is required)')
|
||||
except (subprocess.TimeoutExpired, ValueError):
|
||||
self.logger.warning(
|
||||
f'Failed to check node version. '
|
||||
f'Node returned {returncode} exit status. '
|
||||
f'Node stdout: {stdout}; Node stderr: {stderr}')
|
||||
|
||||
def _real_request_pot(
|
||||
self,
|
||||
request: PoTokenRequest,
|
||||
) -> PoTokenResponse:
|
||||
# used for CI check
|
||||
self.logger.trace(
|
||||
f'Generating POT via script: {self._script_path}')
|
||||
|
||||
command_args = [self._node_path, self._script_path]
|
||||
if proxy := request.request_proxy:
|
||||
command_args.extend(['-p', proxy])
|
||||
command_args.extend(['-c', get_webpo_content_binding(request)[0]])
|
||||
if request.bypass_cache:
|
||||
command_args.append('--bypass-cache')
|
||||
if request.request_source_address:
|
||||
command_args.extend(
|
||||
['--source-address', request.request_source_address])
|
||||
if request.request_verify_tls is False:
|
||||
command_args.append('--disable-tls-verification')
|
||||
|
||||
self.logger.info(
|
||||
f'Generating a {request.context.value} PO Token for '
|
||||
f'{request.internal_client_name} client via bgutil script',
|
||||
)
|
||||
self.logger.debug(
|
||||
f'Executing command to get POT via script: {" ".join(command_args)}')
|
||||
|
||||
try:
|
||||
stdout, stderr, returncode = Popen.run(
|
||||
command_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
timeout=self._GETPOT_TIMEOUT)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
raise PoTokenProviderError(
|
||||
f'_get_pot_via_script failed: Timeout expired when trying to run script (caused by {e!r})')
|
||||
except Exception as e:
|
||||
raise PoTokenProviderError(
|
||||
f'_get_pot_via_script failed: Unable to run script (caused by {e!r})') from e
|
||||
|
||||
msg = ''
|
||||
if stdout_extra := stdout.strip().splitlines()[:-1]:
|
||||
msg = f'stdout:\n{stdout_extra}\n'
|
||||
if stderr_stripped := stderr.strip(): # Empty strings are falsy
|
||||
msg += f'stderr:\n{stderr_stripped}\n'
|
||||
msg = msg.strip()
|
||||
if msg:
|
||||
self.logger.trace(msg)
|
||||
if returncode:
|
||||
raise PoTokenProviderError(
|
||||
f'_get_pot_via_script failed with returncode {returncode}')
|
||||
|
||||
try:
|
||||
json_resp = stdout.splitlines()[-1]
|
||||
self.logger.trace(f'JSON response:\n{json_resp}')
|
||||
# The JSON response is always the last line
|
||||
script_data_resp = json.loads(json_resp)
|
||||
except json.JSONDecodeError as e:
|
||||
raise PoTokenProviderError(
|
||||
f'Error parsing JSON response from _get_pot_via_script (caused by {e!r})') from e
|
||||
if 'poToken' not in script_data_resp:
|
||||
raise PoTokenProviderError(
|
||||
'The script did not respond with a po_token')
|
||||
return PoTokenResponse(po_token=script_data_resp['poToken'])
|
||||
|
||||
|
||||
@register_preference(BgUtilScriptPTP)
|
||||
def bgutil_script_getpot_preference(provider, request):
|
||||
return 1
|
||||
|
||||
|
||||
__all__ = [BgUtilScriptPTP.__name__,
|
||||
bgutil_script_getpot_preference.__name__]
|
||||
@@ -1,6 +1,7 @@
|
||||
[user]
|
||||
name = sudacode
|
||||
email = suda@sudacode.com
|
||||
signingkey = /Users/sudacode/.ssh/yuh.pub
|
||||
[init]
|
||||
defaultBranch = main
|
||||
[push]
|
||||
@@ -19,4 +20,8 @@
|
||||
[color]
|
||||
ui = auto
|
||||
[core]
|
||||
pager = less -FRX
|
||||
pager = delta
|
||||
[gpg]
|
||||
format = ssh
|
||||
[commit]
|
||||
gpgsign = true
|
||||
|
||||
@@ -73,3 +73,5 @@ zstyle ':url-quote-magic:*' url-quotes ''
|
||||
# bind it to both typing and pasting
|
||||
zle -N self-insert url-quote-magic
|
||||
zle -N bracketed-paste bracketed-paste-magic
|
||||
|
||||
alias claude-mem='bun "/home/sudacode/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"'
|
||||
|
||||
@@ -62,3 +62,12 @@ zstyle ':url-quote-magic:*' url-quotes ''
|
||||
# bind it to both typing and pasting
|
||||
zle -N self-insert url-quote-magic
|
||||
zle -N bracketed-paste bracketed-paste-magic
|
||||
|
||||
# bun completions
|
||||
[ -s "/Users/sudacode/.bun/_bun" ] && source "/Users/sudacode/.bun/_bun"
|
||||
|
||||
# bun
|
||||
export BUN_INSTALL="$HOME/.bun"
|
||||
export PATH="$BUN_INSTALL/bin:$PATH"
|
||||
|
||||
alias claude-mem='/Users/sudacode/.bun/bin/bun "/Users/sudacode/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"'
|
||||
|
||||
Reference in New Issue
Block a user