s
This commit is contained in:
@@ -12,6 +12,45 @@ prune_path() {
|
|||||||
rm -rf "$target"
|
rm -rf "$target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensure_profile_preferences() {
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$profile_dir/Default"
|
||||||
|
PROFILE_DIR="$profile_dir" python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
profile_dir = Path(os.environ["PROFILE_DIR"])
|
||||||
|
|
||||||
|
def load_json(path: Path) -> dict:
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_json(path: Path, data: dict) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, separators=(",", ":")),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
for relative_path in ("Default/Preferences", "Local State"):
|
||||||
|
path = profile_dir / relative_path
|
||||||
|
data = load_json(path)
|
||||||
|
extensions = data.setdefault("extensions", {})
|
||||||
|
ui = extensions.setdefault("ui", {})
|
||||||
|
ui["developer_mode"] = True
|
||||||
|
profile = data.setdefault("profile", {})
|
||||||
|
profile["exit_type"] = "Normal"
|
||||||
|
save_json(path, data)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
find_bin() {
|
find_bin() {
|
||||||
if [ "$family" = "google-chrome" ]; then
|
if [ "$family" = "google-chrome" ]; then
|
||||||
command -v google-chrome 2>/dev/null || command -v google-chrome-stable 2>/dev/null || true
|
command -v google-chrome 2>/dev/null || command -v google-chrome-stable 2>/dev/null || true
|
||||||
@@ -67,25 +106,74 @@ prune_path "$profile_dir/Default/Code Cache"
|
|||||||
prune_path "$profile_dir/Default/GPUCache"
|
prune_path "$profile_dir/Default/GPUCache"
|
||||||
prune_path "$profile_dir/Default/DawnGraphiteCache"
|
prune_path "$profile_dir/Default/DawnGraphiteCache"
|
||||||
prune_path "$profile_dir/Default/DawnWebGPUCache"
|
prune_path "$profile_dir/Default/DawnWebGPUCache"
|
||||||
|
ensure_profile_preferences
|
||||||
|
|
||||||
exec "$browser_bin" \
|
browser_class="google-chrome"
|
||||||
${render_flags} \
|
if [ "$family" != "google-chrome" ]; then
|
||||||
--window-position=0,0 \
|
browser_class="chromium"
|
||||||
--display="${DISPLAY:?DISPLAY is required}" \
|
fi
|
||||||
--user-data-dir="$profile_dir" \
|
|
||||||
--password-store=basic \
|
launch_browser() {
|
||||||
--no-first-run \
|
"$browser_bin" \
|
||||||
--start-minimized \
|
${render_flags} \
|
||||||
--force-dark-mode \
|
--window-position=0,0 \
|
||||||
--disable-file-system \
|
--display="${DISPLAY:?DISPLAY is required}" \
|
||||||
--disable-dev-shm-usage \
|
--remote-debugging-address=127.0.0.1 \
|
||||||
--disable-background-networking \
|
--remote-debugging-port=9222 \
|
||||||
--disable-breakpad \
|
--user-data-dir="$profile_dir" \
|
||||||
--disable-default-apps \
|
--password-store=basic \
|
||||||
--disable-extensions \
|
--no-first-run \
|
||||||
--disable-sync \
|
--new-window \
|
||||||
--disable-translate \
|
--start-maximized \
|
||||||
--metrics-recording-only \
|
--force-dark-mode \
|
||||||
--no-default-browser-check \
|
--disable-background-mode \
|
||||||
--no-first-run \
|
--disable-dev-shm-usage \
|
||||||
"$start_url"
|
--disable-background-networking \
|
||||||
|
--disable-breakpad \
|
||||||
|
--disable-default-apps \
|
||||||
|
--disable-sync \
|
||||||
|
--disable-translate \
|
||||||
|
--metrics-recording-only \
|
||||||
|
--no-default-browser-check \
|
||||||
|
--no-first-run \
|
||||||
|
"$start_url" &
|
||||||
|
browser_pid=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
visible_window_count() {
|
||||||
|
if ! command -v xdotool >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
DISPLAY="${DISPLAY:?DISPLAY is required}" xdotool search --onlyvisible --class "$browser_class" 2>/dev/null | wc -l | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
launch_browser
|
||||||
|
|
||||||
|
seen_window=0
|
||||||
|
startup_misses=0
|
||||||
|
closed_misses=0
|
||||||
|
while kill -0 "$browser_pid" 2>/dev/null; do
|
||||||
|
window_count="$(visible_window_count || true)"
|
||||||
|
if [ -n "${window_count:-}" ] && [ "$window_count" -gt 0 ] 2>/dev/null; then
|
||||||
|
seen_window=1
|
||||||
|
startup_misses=0
|
||||||
|
closed_misses=0
|
||||||
|
else
|
||||||
|
if [ "$seen_window" -eq 0 ]; then
|
||||||
|
startup_misses=$((startup_misses + 1))
|
||||||
|
if [ "$startup_misses" -ge 15 ]; then
|
||||||
|
kill -TERM "$browser_pid" 2>/dev/null || true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
closed_misses=$((closed_misses + 1))
|
||||||
|
if [ "$closed_misses" -ge 3 ]; then
|
||||||
|
kill -TERM "$browser_pid" 2>/dev/null || true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
wait "$browser_pid"
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ services:
|
|||||||
- "${NEKO_PROFILE_HOST_DIR_1}:/home/neko/.config/google-chrome"
|
- "${NEKO_PROFILE_HOST_DIR_1}:/home/neko/.config/google-chrome"
|
||||||
- "./policies:/etc/chromium/policies/managed:ro"
|
- "./policies:/etc/chromium/policies/managed:ro"
|
||||||
- "./policies:/etc/opt/chrome/policies/managed:ro"
|
- "./policies:/etc/opt/chrome/policies/managed:ro"
|
||||||
|
- "./provisioning:/var/www/provisioning:ro"
|
||||||
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
|
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
|
||||||
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro"
|
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/${NEKO_BROWSER_SUPERVISOR_CONF}:ro"
|
||||||
|
|
||||||
neko2:
|
neko2:
|
||||||
<<: *neko-common
|
<<: *neko-common
|
||||||
@@ -61,8 +62,9 @@ services:
|
|||||||
- "${NEKO_PROFILE_HOST_DIR_2}:/home/neko/.config/google-chrome"
|
- "${NEKO_PROFILE_HOST_DIR_2}:/home/neko/.config/google-chrome"
|
||||||
- "./policies:/etc/chromium/policies/managed:ro"
|
- "./policies:/etc/chromium/policies/managed:ro"
|
||||||
- "./policies:/etc/opt/chrome/policies/managed:ro"
|
- "./policies:/etc/opt/chrome/policies/managed:ro"
|
||||||
|
- "./provisioning:/var/www/provisioning:ro"
|
||||||
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
|
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
|
||||||
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro"
|
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/${NEKO_BROWSER_SUPERVISOR_CONF}:ro"
|
||||||
|
|
||||||
neko3:
|
neko3:
|
||||||
<<: *neko-common
|
<<: *neko-common
|
||||||
@@ -82,5 +84,6 @@ services:
|
|||||||
- "${NEKO_PROFILE_HOST_DIR_3}:/home/neko/.config/google-chrome"
|
- "${NEKO_PROFILE_HOST_DIR_3}:/home/neko/.config/google-chrome"
|
||||||
- "./policies:/etc/chromium/policies/managed:ro"
|
- "./policies:/etc/chromium/policies/managed:ro"
|
||||||
- "./policies:/etc/opt/chrome/policies/managed:ro"
|
- "./policies:/etc/opt/chrome/policies/managed:ro"
|
||||||
|
- "./provisioning:/var/www/provisioning:ro"
|
||||||
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
|
- "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"
|
||||||
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro"
|
- "./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/${NEKO_BROWSER_SUPERVISOR_CONF}:ro"
|
||||||
|
|||||||
@@ -17,5 +17,19 @@
|
|||||||
"VideoCaptureAllowedUrls": ["https://*.google.com/*", "https://*.youtube.com/*"],
|
"VideoCaptureAllowedUrls": ["https://*.google.com/*", "https://*.youtube.com/*"],
|
||||||
"AudioCaptureAllowedUrls": ["https://*.google.com/*", "https://*.youtube.com/*"],
|
"AudioCaptureAllowedUrls": ["https://*.google.com/*", "https://*.youtube.com/*"],
|
||||||
"DeveloperToolsAvailability": 1,
|
"DeveloperToolsAvailability": 1,
|
||||||
"DefaultGeolocationSetting": 1
|
"DefaultGeolocationSetting": 1,
|
||||||
|
"3rdparty": {
|
||||||
|
"extensions": {
|
||||||
|
"dhdgffkkebhmkfjojejmpbldmpobfkfo": {
|
||||||
|
"jsonImport": [
|
||||||
|
{
|
||||||
|
"hash": "1:1b41fa75bfe51a730bb246022f619811bc508da3176463c86009fe5a0a9f16b5",
|
||||||
|
"url": "http://127.0.0.1:8080/provisioning/tampermonkey-provisioning.json",
|
||||||
|
"haltOnError": false,
|
||||||
|
"installAsSystemScripts": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"name": "YouTube Studio Auto Dismiss",
|
||||||
|
"file_url": "https://greasyfork.org/scripts/557378-youtube-studio-auto-dismiss/code/YouTube%20Studio%20Auto%20Dismiss.user.js",
|
||||||
|
"source": "// ==UserScript==\n// @name YouTube Studio Auto Dismiss\n// @namespace http://tampermonkey.net/\n// @version 1.1\n// @description 每5秒自动检测并点击 YouTube Studio 直播页面的 Dismiss 按钮,支持延迟加载\n// @author You\n// @match https://studio.youtube.com/channel/*/livestreaming\n// @match https://studio.youtube.com/video/*/livestreaming\n// @match https://studio.youtube.com/*/livestreaming\n// @license MIT\n// @grant none\n// @downloadURL https://update.greasyfork.org/scripts/557378/YouTube%20Studio%20Auto%20Dismiss.user.js\n// @updateURL https://update.greasyfork.org/scripts/557378/YouTube%20Studio%20Auto%20Dismiss.meta.js\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n function triggerClick(el) {\n if (!el) return;\n const evt = new MouseEvent(\"click\", {\n view: window,\n bubbles: true,\n cancelable: true,\n });\n el.dispatchEvent(evt);\n console.log(\"Dismiss 按钮已点击:\", el, \"父元素:\", el.parentElement);\n }\n\n function findDismiss(root = document) {\n const allElements = root.querySelectorAll(\"button\");\n for (const el of allElements) {\n try {\n const text = el.innerText ? el.innerText.trim() : \"\";\n if (text === \"Dismiss\" && isElementVisible(el)) {\n console.log(\"找到 Dismiss 按钮:\", el);\n return el;\n }\n\n if (el.shadowRoot) {\n const shadowBtn = findDismiss(el.shadowRoot);\n if (shadowBtn) return shadowBtn;\n }\n } catch (error) {\n console.log(\"检查元素时出错:\", error);\n }\n }\n return null;\n }\n\n function isElementVisible(el) {\n const style = window.getComputedStyle(el);\n return style.display !== \"none\" && style.visibility !== \"hidden\" && el.offsetParent !== null;\n }\n\n function checkAndClick() {\n const btn = findDismiss();\n if (btn) {\n triggerClick(btn);\n } else {\n console.log(\"未找到 Dismiss 按钮或按钮不可见\");\n }\n }\n\n if (document.readyState === \"complete\") {\n checkAndClick();\n } else {\n window.addEventListener(\"load\", checkAndClick, { once: true });\n }\n\n setInterval(checkAndClick, 5000);\n})();\n",
|
||||||
|
"enabled": true,
|
||||||
|
"position": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"runtime_content_mode": "content"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name YouTube Studio Auto Dismiss
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.1
|
||||||
|
// @description 每5秒自动检测并点击 YouTube Studio 直播页面的 Dismiss 按钮,支持延迟加载
|
||||||
|
// @author You
|
||||||
|
// @match https://studio.youtube.com/channel/*/livestreaming
|
||||||
|
// @match https://studio.youtube.com/video/*/livestreaming
|
||||||
|
// @match https://studio.youtube.com/*/livestreaming
|
||||||
|
// @license MIT
|
||||||
|
// @grant none
|
||||||
|
// @downloadURL https://update.greasyfork.org/scripts/557378/YouTube%20Studio%20Auto%20Dismiss.user.js
|
||||||
|
// @updateURL https://update.greasyfork.org/scripts/557378/YouTube%20Studio%20Auto%20Dismiss.meta.js
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function triggerClick(el) {
|
||||||
|
if (!el) return;
|
||||||
|
const evt = new MouseEvent("click", {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
el.dispatchEvent(evt);
|
||||||
|
console.log("Dismiss 按钮已点击:", el, "父元素:", el.parentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDismiss(root = document) {
|
||||||
|
const allElements = root.querySelectorAll("button");
|
||||||
|
for (const el of allElements) {
|
||||||
|
try {
|
||||||
|
const text = el.innerText ? el.innerText.trim() : "";
|
||||||
|
if (text === "Dismiss" && isElementVisible(el)) {
|
||||||
|
console.log("找到 Dismiss 按钮:", el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.shadowRoot) {
|
||||||
|
const shadowBtn = findDismiss(el.shadowRoot);
|
||||||
|
if (shadowBtn) return shadowBtn;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("检查元素时出错:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isElementVisible(el) {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return style.display !== "none" && style.visibility !== "hidden" && el.offsetParent !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndClick() {
|
||||||
|
const btn = findDismiss();
|
||||||
|
if (btn) {
|
||||||
|
triggerClick(btn);
|
||||||
|
} else {
|
||||||
|
console.log("未找到 Dismiss 按钮或按钮不可见");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
checkAndClick();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", checkAndClick, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(checkAndClick, 5000);
|
||||||
|
})();
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -15,8 +16,14 @@ class NekoConfigTests(unittest.TestCase):
|
|||||||
self.assertIn("--disable-software-rasterizer", script)
|
self.assertIn("--disable-software-rasterizer", script)
|
||||||
self.assertIn("--use-gl=swiftshader", script)
|
self.assertIn("--use-gl=swiftshader", script)
|
||||||
self.assertIn("NEKO_BROWSER_RENDER_MODE", script)
|
self.assertIn("NEKO_BROWSER_RENDER_MODE", script)
|
||||||
self.assertIn("--start-minimized", script)
|
self.assertIn("--start-maximized", script)
|
||||||
|
self.assertIn("--disable-background-mode", script)
|
||||||
self.assertIn("--disable-background-networking", script)
|
self.assertIn("--disable-background-networking", script)
|
||||||
|
self.assertNotIn("--disable-extensions", script)
|
||||||
|
self.assertIn('ui["developer_mode"] = True', script)
|
||||||
|
self.assertIn('"Local State"', script)
|
||||||
|
self.assertIn('browser_class="google-chrome"', script)
|
||||||
|
self.assertIn('xdotool search --onlyvisible --class "$browser_class"', script)
|
||||||
self.assertIn('prune_path "$profile_dir/BrowserMetrics"', script)
|
self.assertIn('prune_path "$profile_dir/BrowserMetrics"', script)
|
||||||
self.assertIn('prune_path "$profile_dir/Crash Reports"', script)
|
self.assertIn('prune_path "$profile_dir/Crash Reports"', script)
|
||||||
self.assertIn('prune_path "$profile_dir/SingletonLock"', script)
|
self.assertIn('prune_path "$profile_dir/SingletonLock"', script)
|
||||||
@@ -29,6 +36,10 @@ class NekoConfigTests(unittest.TestCase):
|
|||||||
self.assertTrue(policies["VideoCaptureAllowed"])
|
self.assertTrue(policies["VideoCaptureAllowed"])
|
||||||
self.assertTrue(policies["AudioCaptureAllowed"])
|
self.assertTrue(policies["AudioCaptureAllowed"])
|
||||||
self.assertEqual(policies["DeveloperToolsAvailability"], 1)
|
self.assertEqual(policies["DeveloperToolsAvailability"], 1)
|
||||||
|
tm_policy = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0]
|
||||||
|
self.assertEqual(tm_policy["url"], "http://127.0.0.1:8080/provisioning/tampermonkey-provisioning.json")
|
||||||
|
self.assertTrue(tm_policy["hash"].startswith("1:"))
|
||||||
|
self.assertTrue(tm_policy["installAsSystemScripts"])
|
||||||
|
|
||||||
def test_google_chrome_supervisor_uses_direct_stable_command(self) -> None:
|
def test_google_chrome_supervisor_uses_direct_stable_command(self) -> None:
|
||||||
conf = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
conf = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
||||||
@@ -53,16 +64,16 @@ class NekoConfigTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_neko_compose_mounts_only_selected_supervisor_conf(self) -> None:
|
def test_neko_compose_mounts_only_selected_supervisor_conf(self) -> None:
|
||||||
compose = (ROOT / "services" / "neko" / "docker-compose.yml").read_text(encoding="utf-8")
|
compose = (ROOT / "services" / "neko" / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
self.assertIn("./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro", compose)
|
self.assertIn("./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/${NEKO_BROWSER_SUPERVISOR_CONF}:ro", compose)
|
||||||
self.assertIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose)
|
self.assertIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose)
|
||||||
|
self.assertIn("./provisioning:/var/www/provisioning:ro", compose)
|
||||||
self.assertIn("container_name: live-neko-1", compose)
|
self.assertIn("container_name: live-neko-1", compose)
|
||||||
self.assertIn("container_name: live-neko-2", compose)
|
self.assertIn("container_name: live-neko-2", compose)
|
||||||
self.assertIn("container_name: live-neko-3", compose)
|
self.assertIn("container_name: live-neko-3", compose)
|
||||||
self.assertIn("${NEKO_HTTP_PORT_1}:8080", compose)
|
self.assertIn("${NEKO_HTTP_PORT_1}:8080", compose)
|
||||||
self.assertIn("${NEKO_HTTP_PORT_2}:8080", compose)
|
self.assertIn("${NEKO_HTTP_PORT_2}:8080", compose)
|
||||||
self.assertIn("${NEKO_HTTP_PORT_3}:8080", compose)
|
self.assertIn("${NEKO_HTTP_PORT_3}:8080", compose)
|
||||||
self.assertNotIn("./chromium.conf:/etc/neko/supervisord/chromium.conf:ro", compose)
|
self.assertNotIn("/etc/neko/supervisord/browser.conf:ro", compose)
|
||||||
self.assertNotIn("./google-chrome.conf:/etc/neko/supervisord/google-chrome.conf:ro", compose)
|
|
||||||
|
|
||||||
def test_supervisor_uses_browser_launch_script(self) -> None:
|
def test_supervisor_uses_browser_launch_script(self) -> None:
|
||||||
chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
||||||
@@ -72,6 +83,28 @@ class NekoConfigTests(unittest.TestCase):
|
|||||||
self.assertNotIn("--start-maximized", chrome)
|
self.assertNotIn("--start-maximized", chrome)
|
||||||
self.assertNotIn("--start-maximized", chromium)
|
self.assertNotIn("--start-maximized", chromium)
|
||||||
|
|
||||||
|
def test_tampermonkey_provisioning_matches_policy_hash(self) -> None:
|
||||||
|
provisioning_path = ROOT / "services" / "neko" / "provisioning" / "tampermonkey-provisioning.json"
|
||||||
|
userscript_path = ROOT / "services" / "neko" / "provisioning" / "youtube-studio-auto-dismiss.user.js"
|
||||||
|
payload = json.loads(provisioning_path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(payload["version"], "1")
|
||||||
|
self.assertEqual(payload["settings"]["runtime_content_mode"], "content")
|
||||||
|
self.assertEqual(payload["scripts"][0]["file_url"], "https://greasyfork.org/scripts/557378-youtube-studio-auto-dismiss/code/YouTube%20Studio%20Auto%20Dismiss.user.js")
|
||||||
|
self.assertEqual(payload["scripts"][0]["source"], userscript_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"render_tampermonkey_provisioning",
|
||||||
|
ROOT / "tools" / "render_tampermonkey_provisioning.py",
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(spec)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec and spec.loader
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
policies = json.loads((ROOT / "services" / "neko" / "policies" / "policies.json").read_text(encoding="utf-8"))
|
||||||
|
expected_hash = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0]["hash"]
|
||||||
|
self.assertEqual(expected_hash, f"1:{module.tm_hash(payload)}")
|
||||||
|
|
||||||
def test_neko_scripts_use_family_specific_profile_dirs(self) -> None:
|
def test_neko_scripts_use_family_specific_profile_dirs(self) -> None:
|
||||||
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")
|
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")
|
||||||
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
||||||
|
|||||||
91
d2ypp2/tools/render_tampermonkey_provisioning.py
Normal file
91
d2ypp2/tools/render_tampermonkey_provisioning.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
DEFAULT_SCRIPT = ROOT / "services" / "neko" / "provisioning" / "youtube-studio-auto-dismiss.user.js"
|
||||||
|
DEFAULT_OUTPUT = ROOT / "services" / "neko" / "provisioning" / "tampermonkey-provisioning.json"
|
||||||
|
GREASYFORK_URL = "https://greasyfork.org/scripts/557378-youtube-studio-auto-dismiss/code/YouTube%20Studio%20Auto%20Dismiss.user.js"
|
||||||
|
|
||||||
|
|
||||||
|
def tm_sha256(text: str) -> str:
|
||||||
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def tm_hash(value: Any, seen: list[int] | None = None) -> str:
|
||||||
|
if seen is None:
|
||||||
|
seen = []
|
||||||
|
if value is None:
|
||||||
|
return tm_sha256("object:null")
|
||||||
|
if isinstance(value, dict):
|
||||||
|
ident = id(value)
|
||||||
|
if ident in seen:
|
||||||
|
raise ValueError("Found circular structure")
|
||||||
|
seen.append(ident)
|
||||||
|
try:
|
||||||
|
parts = [tm_hash(value[key], seen) for key in sorted(value)]
|
||||||
|
finally:
|
||||||
|
seen.pop()
|
||||||
|
return tm_sha256("".join(parts))
|
||||||
|
if isinstance(value, list):
|
||||||
|
ident = id(value)
|
||||||
|
if ident in seen:
|
||||||
|
raise ValueError("Found circular structure")
|
||||||
|
seen.append(ident)
|
||||||
|
try:
|
||||||
|
parts = [tm_hash(item, seen) for item in value]
|
||||||
|
finally:
|
||||||
|
seen.pop()
|
||||||
|
return tm_sha256("".join(parts))
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return tm_sha256(f"boolean:{str(value).lower()}")
|
||||||
|
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||||
|
return tm_sha256(f"number:{value}")
|
||||||
|
if isinstance(value, str):
|
||||||
|
return tm_sha256(f"string:{value}")
|
||||||
|
raise TypeError(f"Unsupported provisioning value: {type(value)!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_payload(script_text: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"version": "1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"name": "YouTube Studio Auto Dismiss",
|
||||||
|
"file_url": GREASYFORK_URL,
|
||||||
|
"source": script_text,
|
||||||
|
"enabled": True,
|
||||||
|
"position": 1,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"runtime_content_mode": "content",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Render Tampermonkey provisioning JSON and hash.")
|
||||||
|
parser.add_argument("--script", type=Path, default=DEFAULT_SCRIPT)
|
||||||
|
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||||
|
parser.add_argument("--write", action="store_true", help="Write the rendered JSON to --output.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
script_text = args.script.read_text(encoding="utf-8").strip() + "\n"
|
||||||
|
payload = build_payload(script_text)
|
||||||
|
rendered = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
||||||
|
if args.write:
|
||||||
|
args.output.write_text(rendered, encoding="utf-8")
|
||||||
|
print(rendered, end="")
|
||||||
|
print(f"hash=1:{tm_hash(payload)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user