Compare commits

...

11 Commits

Author SHA1 Message Date
17a8347a3a update cookies 2026-02-08 02:43:03 -08:00
2cfda7d222 update cookies 2026-02-08 01:23:19 -08:00
09e4038309 Merge branch 'master' of github.com:ksyasuda/dotfiles 2026-02-08 01:10:40 -08:00
8bab55f793 update cookies 2026-02-08 01:10:34 -08:00
b707558d36 update 2026-02-08 00:02:43 -08:00
4bb15f7f29 update 2026-02-07 23:54:38 -08:00
781262a881 update 2026-02-07 23:52:21 -08:00
8e171bf47f add plugins 2026-02-07 23:51:18 -08:00
0e369ee61d update 2026-02-07 17:18:47 -08:00
494226f524 add noto sans cjk jp 2026-02-07 17:18:36 -08:00
1d8e65d4e2 update 2026-02-07 14:53:38 -08:00
26 changed files with 624 additions and 525 deletions

View File

@@ -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,

View File

@@ -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"]
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -271,3 +271,10 @@ debug {
disable_logs = true
enable_stdout_logs = false
}
layerrule {
name = fix-rofi
match:namespace = rofi
no_anim = true
}

View File

@@ -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"

Binary file not shown.

View File

@@ -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"

View File

@@ -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]

View File

@@ -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

View File

@@ -1 +0,0 @@
../submodules/animecards/animecards

View File

@@ -1 +0,0 @@
../submodules/autosubsync-mpv

View File

@@ -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
);

View File

@@ -1 +0,0 @@
../submodules/mpvacious

View File

@@ -1 +0,0 @@
../submodules/mpv-youtube-upnext/youtube-upnext.lua

View File

@@ -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 = {

View File

@@ -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
View 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",
},
}

View File

@@ -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"

View File

@@ -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__']

View File

@@ -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__]

View File

@@ -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__]

View File

@@ -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

View File

@@ -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"'

View File

@@ -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"'