mirror of
https://github.com/andwati/kget-capture.git
synced 2026-10-18 03:02:56 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
keys/*.pem
|
||||||
|
__pycache__/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Ian
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# KGet Capture
|
||||||
|
|
||||||
|
A Chrome (and other Chromium-browser) extension that sends downloads to
|
||||||
|
[KGet](https://apps.kde.org/kget/), KDE's download manager, instead of (or in
|
||||||
|
addition to) the browser's built-in downloader.
|
||||||
|
|
||||||
|
- Right-click any link, image, video, or audio element and choose
|
||||||
|
**"Download with KGet"** to open KGet's "New Download" dialog, prefilled
|
||||||
|
with the URL — edit the filename or destination folder if you want, then
|
||||||
|
confirm to start the transfer.
|
||||||
|
- A toolbar toggle enables **auto-capture**: while on, every download Chrome
|
||||||
|
starts is cancelled and redirected to the same KGet dialog automatically.
|
||||||
|
Defaults to ON.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux with KGet installed (and running, or set to autostart)
|
||||||
|
- One of: Google Chrome, Chromium, Brave, Vivaldi, Microsoft Edge
|
||||||
|
- `python3` and `dbus-send` on `PATH` (standard on virtually all Linux/KDE
|
||||||
|
systems already)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <this-repo-url> kget-capture
|
||||||
|
cd kget-capture
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`install.sh` registers a native messaging host for every supported browser it
|
||||||
|
finds installed, then prints the extension's fixed ID and next steps:
|
||||||
|
|
||||||
|
1. Open `chrome://extensions` (or your browser's equivalent).
|
||||||
|
2. Enable **Developer mode** (top-right toggle).
|
||||||
|
3. Click **Load unpacked** and select this repo's `extension/` directory.
|
||||||
|
4. Confirm the loaded extension's ID matches the one `install.sh` printed.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- **On-demand:** right-click a link/image/video/audio → "Download with KGet".
|
||||||
|
- **Automatic:** click the toolbar icon and flip "Auto-capture all downloads"
|
||||||
|
on or off. The badge shows "ON" (green) or "OFF" (grey).
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This removes the native messaging host registrations and the installed host
|
||||||
|
script. You'll still need to remove the unpacked extension yourself via
|
||||||
|
`chrome://extensions` — that step can't be scripted.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- **`blob:`/`data:` URLs can't be captured.** There's no real network URL to
|
||||||
|
hand to KGet, so these downloads proceed normally through Chrome (a console
|
||||||
|
warning is logged in the extension's service worker).
|
||||||
|
- **Auto-capture has a brief head start.** Manifest V3 removed blocking
|
||||||
|
`webRequest`, so Chrome begins the download before the extension can cancel
|
||||||
|
it. A small/partial temp file may transiently appear before it's erased.
|
||||||
|
- **A KGet dialog opens for every capture.** The extension uses KGet's
|
||||||
|
`showNewTransferDialog` D-Bus method, which opens the interactive "New
|
||||||
|
Download" dialog rather than starting the transfer immediately. This is
|
||||||
|
intentional — it lets you rename the file or change the destination folder
|
||||||
|
before it starts. If you'd rather it start immediately without a prompt,
|
||||||
|
see [Troubleshooting](#troubleshooting) for the alternate `addTransfer`
|
||||||
|
call — note that route only returns once the dialog is dismissed, so it's
|
||||||
|
not what the extension uses by default.
|
||||||
|
- **KGet must already be running.** If it's not, captures fail silently from
|
||||||
|
the user's perspective — check the service worker console for the actual
|
||||||
|
error (see Troubleshooting).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Check native host registration:** confirm
|
||||||
|
`~/.config/<browser>/NativeMessagingHosts/com.kget_capture.host.json` exists
|
||||||
|
and its `path` points at an executable file
|
||||||
|
(`~/.local/share/kget-capture/kget_capture_host.py`).
|
||||||
|
- **Inspect the extension's logs:** on `chrome://extensions`, find "KGet
|
||||||
|
Capture", click "service worker" under "Inspect views" to open its console.
|
||||||
|
- **Test D-Bus directly:**
|
||||||
|
```bash
|
||||||
|
dbus-send --print-reply --dest=org.kde.kget /KGet \
|
||||||
|
org.kde.kget.main.showNewTransferDialog array:string:"https://example.com/file.zip"
|
||||||
|
```
|
||||||
|
If this fails with `ServiceUnknown`, KGet isn't running. To skip the dialog
|
||||||
|
and start a transfer straight away instead, use `addTransfer` with a real
|
||||||
|
destination folder:
|
||||||
|
```bash
|
||||||
|
dbus-send --print-reply --dest=org.kde.kget /KGet \
|
||||||
|
org.kde.kget.main.addTransfer string:"https://example.com/file.zip" string:"$HOME/Downloads" boolean:true
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
const NATIVE_HOST = "com.kget_capture.host";
|
||||||
|
const MENU_ID = "kget-capture-download";
|
||||||
|
|
||||||
|
function updateBadge(enabled) {
|
||||||
|
chrome.action.setBadgeText({ text: enabled ? "ON" : "OFF" });
|
||||||
|
chrome.action.setBadgeBackgroundColor({ color: enabled ? "#2e7d32" : "#9e9e9e" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshBadge() {
|
||||||
|
const { autoCaptureEnabled } = await chrome.storage.local.get({ autoCaptureEnabled: true });
|
||||||
|
updateBadge(autoCaptureEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToKGet(url) {
|
||||||
|
chrome.runtime.sendNativeMessage(NATIVE_HOST, { action: "addTransfer", url }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error("[kget-capture] native host error:", chrome.runtime.lastError.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.error("[kget-capture] KGet reported failure:", response && response.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(async (details) => {
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: MENU_ID,
|
||||||
|
title: "Download with KGet",
|
||||||
|
contexts: ["link", "image", "video", "audio"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (details.reason === "install") {
|
||||||
|
await chrome.storage.local.set({ autoCaptureEnabled: true });
|
||||||
|
}
|
||||||
|
refreshBadge();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.contextMenus.onClicked.addListener((info) => {
|
||||||
|
if (info.menuItemId !== MENU_ID) return;
|
||||||
|
const url = info.linkUrl || info.srcUrl;
|
||||||
|
if (!url) return;
|
||||||
|
sendToKGet(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.downloads.onCreated.addListener(async (item) => {
|
||||||
|
const { autoCaptureEnabled } = await chrome.storage.local.get({ autoCaptureEnabled: true });
|
||||||
|
if (!autoCaptureEnabled) return;
|
||||||
|
|
||||||
|
const url = item.finalUrl || item.url;
|
||||||
|
if (url.startsWith("blob:") || url.startsWith("data:")) {
|
||||||
|
console.warn("[kget-capture] cannot redirect blob:/data: URL, letting Chrome handle it:", url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.downloads.cancel(item.id);
|
||||||
|
// erase() only clears the history entry -- it does NOT delete the
|
||||||
|
// partial file Chrome already wrote to disk. removeFile() does that.
|
||||||
|
await chrome.downloads.removeFile(item.id);
|
||||||
|
} catch (e) {
|
||||||
|
// Expected if Chrome finished the download before cancel() landed, or
|
||||||
|
// hadn't written any bytes yet -- not worth surfacing to the user.
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await chrome.downloads.erase({ id: item.id });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[kget-capture] failed to erase Chrome download entry:", e);
|
||||||
|
}
|
||||||
|
sendToKGet(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.storage.onChanged.addListener((changes, area) => {
|
||||||
|
if (area === "local" && "autoCaptureEnabled" in changes) {
|
||||||
|
updateBadge(changes.autoCaptureEnabled.newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBadge();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 651 B |
Binary file not shown.
|
After Width: | Height: | Size: 1018 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "KGet Capture",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Send browser downloads to KGet (KDE Download Manager) on Linux.",
|
||||||
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtM1nFg5pPd3UBzynUJHj6aUy7bjudZaX9AQTWGMqAX3fXylw1lubSzP1/BaS9ivvgOJg/loyEgzEZlcNPzblFVlg8BqEJ3tntdk2OzYvUs0pUdcPzzyjso9TykCkUUTI7ev8JkreW/FYlinfvNwSYN4/c1s3h8/I6XHtxOdmO1G3bS98+BQaXPldbDmxoKwyWR8u1f8gTO2Mg3GY4BHau3RWf1kgtNzh20p9wE3sEFdWkFu7ArgRCIrWLh6m3O6lolnHQSCWPwtOTx893Qkp9Eu6FwUnoWc8rsYx8sqa0eNCTCS6Kjr1Iu3yEJ5y9390jUiDZTQ/c62ueCTQaYv2bwIDAQAB",
|
||||||
|
"permissions": ["downloads", "contextMenus", "storage", "nativeMessaging"],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
body {
|
||||||
|
width: 240px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #777;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>KGet Capture</h1>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" id="auto-capture-toggle">
|
||||||
|
<span>Auto-capture all downloads</span>
|
||||||
|
</label>
|
||||||
|
<p class="status" id="status-line"></p>
|
||||||
|
<p class="hint">Right-click any link, image, video, or audio and choose
|
||||||
|
"Download with KGet" to send it to KGet on demand.</p>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const checkbox = document.getElementById("auto-capture-toggle");
|
||||||
|
const statusLine = document.getElementById("status-line");
|
||||||
|
|
||||||
|
function renderStatus(enabled) {
|
||||||
|
statusLine.textContent = `Auto-capture: ${enabled ? "ON" : "OFF"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.storage.local.get({ autoCaptureEnabled: true }).then(({ autoCaptureEnabled }) => {
|
||||||
|
checkbox.checked = autoCaptureEnabled;
|
||||||
|
renderStatus(autoCaptureEnabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkbox.addEventListener("change", () => {
|
||||||
|
chrome.storage.local.set({ autoCaptureEnabled: checkbox.checked });
|
||||||
|
renderStatus(checkbox.checked);
|
||||||
|
});
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
INSTALL_DIR="$HOME/.local/share/kget-capture"
|
||||||
|
HOST_SCRIPT_SRC="$SCRIPT_DIR/native-host/kget_capture_host.py"
|
||||||
|
HOST_SCRIPT_DEST="$INSTALL_DIR/kget_capture_host.py"
|
||||||
|
MANIFEST_TEMPLATE="$SCRIPT_DIR/native-host/com.kget_capture.host.json"
|
||||||
|
GENERATED_MANIFEST="$INSTALL_DIR/com.kget_capture.host.json"
|
||||||
|
|
||||||
|
echo "Checking dependencies..."
|
||||||
|
for dep in python3 dbus-send; do
|
||||||
|
if ! command -v "$dep" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: '$dep' is required but not found on PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Installing native host script to $HOST_SCRIPT_DEST"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
cp "$HOST_SCRIPT_SRC" "$HOST_SCRIPT_DEST"
|
||||||
|
chmod +x "$HOST_SCRIPT_DEST"
|
||||||
|
|
||||||
|
echo "Generating native messaging host manifest..."
|
||||||
|
sed "s|__HOST_SCRIPT_PATH__|$HOST_SCRIPT_DEST|" "$MANIFEST_TEMPLATE" > "$GENERATED_MANIFEST"
|
||||||
|
|
||||||
|
declare -A BROWSER_DIRS=(
|
||||||
|
["Google Chrome"]="$HOME/.config/google-chrome"
|
||||||
|
["Chromium"]="$HOME/.config/chromium"
|
||||||
|
["Brave"]="$HOME/.config/BraveSoftware/Brave-Browser"
|
||||||
|
["Vivaldi"]="$HOME/.config/vivaldi"
|
||||||
|
["Microsoft Edge"]="$HOME/.config/microsoft-edge"
|
||||||
|
)
|
||||||
|
|
||||||
|
registered_any=false
|
||||||
|
for browser in "${!BROWSER_DIRS[@]}"; do
|
||||||
|
config_dir="${BROWSER_DIRS[$browser]}"
|
||||||
|
if [ -d "$config_dir" ]; then
|
||||||
|
target_dir="$config_dir/NativeMessagingHosts"
|
||||||
|
mkdir -p "$target_dir"
|
||||||
|
cp "$GENERATED_MANIFEST" "$target_dir/com.kget_capture.host.json"
|
||||||
|
echo "Registered native host for $browser"
|
||||||
|
registered_any=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$registered_any" = false ]; then
|
||||||
|
echo "WARNING: no supported browser config directories were found. Checked:"
|
||||||
|
for browser in "${!BROWSER_DIRS[@]}"; do
|
||||||
|
echo " - ${BROWSER_DIRS[$browser]}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXT_ID=$(cat "$SCRIPT_DIR/keys/extension-id.txt")
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Install complete.
|
||||||
|
|
||||||
|
Extension ID: $EXT_ID
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
1. Open chrome://extensions (or the equivalent for your browser)
|
||||||
|
2. Enable "Developer mode" (top-right toggle)
|
||||||
|
3. Click "Load unpacked" and select: $SCRIPT_DIR/extension
|
||||||
|
4. Confirm the loaded extension's ID matches: $EXT_ID
|
||||||
|
|
||||||
|
Make sure KGet is running (or set to autostart) for captures to work.
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Extension signing key
|
||||||
|
|
||||||
|
This directory holds the RSA key pair used to pin the extension's Chrome ID so
|
||||||
|
it's identical across every install (needed so the native messaging host's
|
||||||
|
`allowed_origins` can be hardcoded instead of asking each user to paste their
|
||||||
|
extension ID).
|
||||||
|
|
||||||
|
- `kget-capture-private.pem` — **not committed** (see `.gitignore`). Keep it
|
||||||
|
somewhere safe if you want to be able to reproduce the same key deterministically
|
||||||
|
later; losing it just means a future re-key would get a new extension ID, it's
|
||||||
|
not a security incident.
|
||||||
|
- `kget-capture-public.der` / `kget-capture-public.b64` — derived from the
|
||||||
|
private key. The base64 contents of `kget-capture-public.b64` is pasted
|
||||||
|
verbatim into `extension/manifest.json`'s `"key"` field. These are already
|
||||||
|
effectively public once checked into `manifest.json`, so committing them is
|
||||||
|
fine (and `extension-id.txt` too).
|
||||||
|
- `extension-id.txt` — the 32-character Chrome extension ID derived from the
|
||||||
|
public key (also hardcoded into `native-host/com.kget_capture.host.json`).
|
||||||
|
|
||||||
|
## Regenerating (only if rotating the key)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl genrsa -out keys/kget-capture-private.pem 2048
|
||||||
|
openssl rsa -in keys/kget-capture-private.pem -pubout -outform DER -out keys/kget-capture-public.der
|
||||||
|
openssl base64 -A -in keys/kget-capture-public.der -out keys/kget-capture-public.b64
|
||||||
|
openssl dgst -sha256 -binary keys/kget-capture-public.der | head -c 16 | od -An -tx1 \
|
||||||
|
| tr -d ' \n' | tr '0123456789abcdef' 'abcdefghijklmnop' > keys/extension-id.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Rotating the key changes the extension ID, which means `extension/manifest.json`'s
|
||||||
|
`"key"` and `native-host/com.kget_capture.host.json`'s `allowed_origins` both
|
||||||
|
need updating to match.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
lgkhlfhejbffjnpcdlpjclicihnbjijp
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtM1nFg5pPd3UBzynUJHj6aUy7bjudZaX9AQTWGMqAX3fXylw1lubSzP1/BaS9ivvgOJg/loyEgzEZlcNPzblFVlg8BqEJ3tntdk2OzYvUs0pUdcPzzyjso9TykCkUUTI7ev8JkreW/FYlinfvNwSYN4/c1s3h8/I6XHtxOdmO1G3bS98+BQaXPldbDmxoKwyWR8u1f8gTO2Mg3GY4BHau3RWf1kgtNzh20p9wE3sEFdWkFu7ArgRCIrWLh6m3O6lolnHQSCWPwtOTx893Qkp9Eu6FwUnoWc8rsYx8sqa0eNCTCS6Kjr1Iu3yEJ5y9390jUiDZTQ/c62ueCTQaYv2bwIDAQAB
|
||||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "com.kget_capture.host",
|
||||||
|
"description": "Native messaging host that hands off download URLs to KGet via D-Bus",
|
||||||
|
"path": "__HOST_SCRIPT_PATH__",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": ["chrome-extension://lgkhlfhejbffjnpcdlpjclicihnbjijp/"]
|
||||||
|
}
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Native messaging host bridging Chrome to KGet's D-Bus service."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_message():
|
||||||
|
raw_length = sys.stdin.buffer.read(4)
|
||||||
|
if len(raw_length) < 4:
|
||||||
|
return None
|
||||||
|
(length,) = struct.unpack("<I", raw_length)
|
||||||
|
data = sys.stdin.buffer.read(length)
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
def write_message(message):
|
||||||
|
data = json.dumps(message).encode("utf-8")
|
||||||
|
sys.stdout.buffer.write(struct.pack("<I", len(data)))
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def add_transfer(url):
|
||||||
|
# showNewTransferDialog opens KGet's "New Download" dialog (editable name
|
||||||
|
# and destination) and returns immediately without waiting for the user
|
||||||
|
# to interact with it. addTransfer, by contrast, only returns once the
|
||||||
|
# dialog is dismissed when destDir is empty -- it blocks the D-Bus call
|
||||||
|
# on human input, which is why we don't use it here.
|
||||||
|
cmd = [
|
||||||
|
"dbus-send",
|
||||||
|
"--print-reply",
|
||||||
|
"--reply-timeout=10000",
|
||||||
|
"--dest=org.kde.kget",
|
||||||
|
"/KGet",
|
||||||
|
"org.kde.kget.main.showNewTransferDialog",
|
||||||
|
f"array:string:{url}",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "dbus-send not found on this system"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "dbus-send call timed out (is KGet running?)"
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
stderr = result.stderr.strip()
|
||||||
|
return False, f"dbus-send failed: {stderr or 'unknown error'}"
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
message = read_message()
|
||||||
|
if not message or message.get("action") != "addTransfer" or "url" not in message:
|
||||||
|
write_message({"ok": False, "error": "invalid request"})
|
||||||
|
return
|
||||||
|
|
||||||
|
ok, error = add_transfer(message["url"])
|
||||||
|
write_message({"ok": True} if ok else {"ok": False, "error": error})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
INSTALL_DIR="$HOME/.local/share/kget-capture"
|
||||||
|
|
||||||
|
BROWSER_DIRS=(
|
||||||
|
"$HOME/.config/google-chrome"
|
||||||
|
"$HOME/.config/chromium"
|
||||||
|
"$HOME/.config/BraveSoftware/Brave-Browser"
|
||||||
|
"$HOME/.config/vivaldi"
|
||||||
|
"$HOME/.config/microsoft-edge"
|
||||||
|
)
|
||||||
|
|
||||||
|
for config_dir in "${BROWSER_DIRS[@]}"; do
|
||||||
|
manifest="$config_dir/NativeMessagingHosts/com.kget_capture.host.json"
|
||||||
|
if [ -f "$manifest" ]; then
|
||||||
|
rm -f "$manifest"
|
||||||
|
echo "Removed $manifest"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
echo "Removed $INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Uninstall complete.
|
||||||
|
|
||||||
|
Note: this does not remove the unpacked extension itself. Remove it manually
|
||||||
|
via chrome://extensions (or the equivalent for your browser).
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user