2 Commits
Author SHA1 Message Date
andwati c395c5fbe2 Bump version to 1.0.1 2026-08-12 10:01:26 +03:00
andwati 64c3a55df4 Get correct filenames, skip KGet's dialog, and add a finished notification
Switch auto-capture from onCreated to onDeterminingFilename, which
fires once Chrome has already resolved the real filename (from
Content-Disposition or the URL). That name is now passed through to
the native host and used to build a full dir+filename destination for
addTransfer, replacing showNewTransferDialog. A non-empty destDir
makes KGet skip its interactive dialog and start immediately, which
also fixes it showing an opaque URL path segment as the filename for
signed/obfuscated URLs (e.g. cloud storage links) instead of the real
name.

Also enable KGet's existing (but disabled-by-default) "Download
Finished" KNotify popup via install.sh, so there's still feedback now
that captures no longer show an interactive dialog.
2026-08-12 09:46:54 +03:00
5 changed files with 107 additions and 44 deletions
+35 -21
View File
@@ -5,12 +5,14 @@ A Chrome (and other Chromium-browser) extension that sends downloads to
addition to) the browser's built-in downloader. addition to) the browser's built-in downloader.
- Right-click any link, image, video, or audio element and choose - Right-click any link, image, video, or audio element and choose
**"Download with KGet"** to open KGet's "New Download" dialog, prefilled **"Download with KGet"** to send it straight to KGet — it starts
with the URL — edit the filename or destination folder if you want, then downloading immediately into your Downloads folder, no dialog.
confirm to start the transfer.
- A toolbar toggle enables **auto-capture**: while on, every download Chrome - A toolbar toggle enables **auto-capture**: while on, every download Chrome
starts is cancelled and redirected to the same KGet dialog automatically. starts is cancelled and handed off to KGet the same way, automatically,
Defaults to ON. with the correct filename (resolved by Chrome itself, so it's right even
for URLs whose path is just an opaque ID). Defaults to ON.
- KGet's own "Download Finished" desktop notification is enabled on install,
so you still get feedback once a capture completes.
## Requirements ## Requirements
@@ -18,6 +20,8 @@ addition to) the browser's built-in downloader.
- One of: Google Chrome, Chromium, Brave, Vivaldi, Microsoft Edge - One of: Google Chrome, Chromium, Brave, Vivaldi, Microsoft Edge
- `python3` and `dbus-send` on `PATH` (standard on virtually all Linux/KDE - `python3` and `dbus-send` on `PATH` (standard on virtually all Linux/KDE
systems already) systems already)
- `kwriteconfig6` (ships with KDE Plasma) — optional, only used to enable the
download-finished notification; install still works without it
## Install ## Install
@@ -72,14 +76,25 @@ git push origin v1.0.0
- **Auto-capture has a brief head start.** Manifest V3 removed blocking - **Auto-capture has a brief head start.** Manifest V3 removed blocking
`webRequest`, so Chrome begins the download before the extension can cancel `webRequest`, so Chrome begins the download before the extension can cancel
it. A small/partial temp file may transiently appear before it's erased. 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 - **No editable dialog.** Captures start immediately with no chance to
`showNewTransferDialog` D-Bus method, which opens the interactive "New rename the file or pick a different folder beforehand — rename/move it
Download" dialog rather than starting the transfer immediately. This is afterward in KGet or your file manager if needed. This is intentional: KGet
intentional — it lets you rename the file or change the destination folder only skips its interactive "New Download" dialog when given a real
before it starts. If you'd rather it start immediately without a prompt, destination path up front, so instant capture and an editable prompt are
see [Troubleshooting](#troubleshooting) for the alternate `addTransfer` mutually exclusive given KGet's D-Bus API.
call — note that route only returns once the dialog is dismissed, so it's - **The context-menu capture can't always get the correct filename.**
not what the extension uses by default. Auto-capture gets the real filename for free from Chrome's own
`Content-Disposition` resolution. Right-click captures don't have that —
the extension only has the raw URL — so KGet falls back to guessing from
the URL path, which is wrong for opaque/signed URLs (e.g. cloud storage
links). Fixing this would require broad host permissions and an extra HTTP
request per right-click, which isn't worth it for now.
- **Chrome may still show its own "Save As" dialog.** This is controlled by
Chrome's own **Ask where to save each file before downloading** setting
(`chrome://settings/downloads`) — no extension API can read or suppress
it, and if it's on, Chrome shows the picker *before* our extension gets a
chance to see the download at all. Turn that setting off for auto-capture
to work without an extra prompt.
- **KGet must already be running.** If it's not, captures fail silently from - **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 the user's perspective — check the service worker console for the actual
error (see Troubleshooting). error (see Troubleshooting).
@@ -95,15 +110,14 @@ git push origin v1.0.0
- **Test D-Bus directly:** - **Test D-Bus directly:**
```bash ```bash
dbus-send --print-reply --dest=org.kde.kget /KGet \ dbus-send --print-reply --dest=org.kde.kget /KGet \
org.kde.kget.main.showNewTransferDialog array:string:"https://example.com/file.zip" org.kde.kget.main.addTransfer string:"https://example.com/file.zip" string:"$HOME/Downloads/file.zip" boolean:true
```
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
``` ```
If this fails with `ServiceUnknown`, KGet isn't running.
- **Download-finished notification not showing?** `install.sh` enables it via
`kwriteconfig6 --file knotifyrc --group "Event/kget/finished" --key Action Popup`.
Check System Settings → Notifications → Applications → "KGet Download
Manager" to confirm it's on, or disable it the same way with
`kwriteconfig6 --file knotifyrc --group "Event/kget/finished" --key Action None`.
## License ## License
+30 -13
View File
@@ -11,16 +11,20 @@ async function refreshBadge() {
updateBadge(autoCaptureEnabled); updateBadge(autoCaptureEnabled);
} }
function sendToKGet(url) { function sendToKGet(url, filename) {
chrome.runtime.sendNativeMessage(NATIVE_HOST, { action: "addTransfer", url }, (response) => { chrome.runtime.sendNativeMessage(
if (chrome.runtime.lastError) { NATIVE_HOST,
console.error("[kget-capture] native host error:", chrome.runtime.lastError.message); { action: "addTransfer", url, filename: filename || null },
return; (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);
}
} }
if (!response || !response.ok) { );
console.error("[kget-capture] KGet reported failure:", response && response.error);
}
});
} }
chrome.runtime.onInstalled.addListener(async (details) => { chrome.runtime.onInstalled.addListener(async (details) => {
@@ -40,10 +44,21 @@ chrome.contextMenus.onClicked.addListener((info) => {
if (info.menuItemId !== MENU_ID) return; if (info.menuItemId !== MENU_ID) return;
const url = info.linkUrl || info.srcUrl; const url = info.linkUrl || info.srcUrl;
if (!url) return; if (!url) return;
sendToKGet(url); sendToKGet(url, null);
}); });
chrome.downloads.onCreated.addListener(async (item) => { // onDeterminingFilename (rather than onCreated) fires once Chrome has
// already resolved the real filename (from Content-Disposition or the URL),
// which is exactly what we need to hand KGet the correct name instead of an
// opaque URL path segment.
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
// We're about to cancel this download regardless, so Chrome's own choice
// doesn't matter -- just let it proceed so the event resolves promptly.
suggest();
captureDownload(item);
});
async function captureDownload(item) {
const { autoCaptureEnabled } = await chrome.storage.local.get({ autoCaptureEnabled: true }); const { autoCaptureEnabled } = await chrome.storage.local.get({ autoCaptureEnabled: true });
if (!autoCaptureEnabled) return; if (!autoCaptureEnabled) return;
@@ -67,8 +82,10 @@ chrome.downloads.onCreated.addListener(async (item) => {
} catch (e) { } catch (e) {
console.error("[kget-capture] failed to erase Chrome download entry:", e); console.error("[kget-capture] failed to erase Chrome download entry:", e);
} }
sendToKGet(url);
}); const filename = item.filename ? item.filename.split(/[\\/]/).pop() : null;
sendToKGet(url, filename);
}
chrome.storage.onChanged.addListener((changes, area) => { chrome.storage.onChanged.addListener((changes, area) => {
if (area === "local" && "autoCaptureEnabled" in changes) { if (area === "local" && "autoCaptureEnabled" in changes) {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "KGet Capture", "name": "KGet Capture",
"version": "1.0.0", "version": "1.0.1",
"description": "Send browser downloads to KGet (KDE Download Manager) on Linux.", "description": "Send browser downloads to KGet (KDE Download Manager) on Linux.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtM1nFg5pPd3UBzynUJHj6aUy7bjudZaX9AQTWGMqAX3fXylw1lubSzP1/BaS9ivvgOJg/loyEgzEZlcNPzblFVlg8BqEJ3tntdk2OzYvUs0pUdcPzzyjso9TykCkUUTI7ev8JkreW/FYlinfvNwSYN4/c1s3h8/I6XHtxOdmO1G3bS98+BQaXPldbDmxoKwyWR8u1f8gTO2Mg3GY4BHau3RWf1kgtNzh20p9wE3sEFdWkFu7ArgRCIrWLh6m3O6lolnHQSCWPwtOTx893Qkp9Eu6FwUnoWc8rsYx8sqa0eNCTCS6Kjr1Iu3yEJ5y9390jUiDZTQ/c62ueCTQaYv2bwIDAQAB", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtM1nFg5pPd3UBzynUJHj6aUy7bjudZaX9AQTWGMqAX3fXylw1lubSzP1/BaS9ivvgOJg/loyEgzEZlcNPzblFVlg8BqEJ3tntdk2OzYvUs0pUdcPzzyjso9TykCkUUTI7ev8JkreW/FYlinfvNwSYN4/c1s3h8/I6XHtxOdmO1G3bS98+BQaXPldbDmxoKwyWR8u1f8gTO2Mg3GY4BHau3RWf1kgtNzh20p9wE3sEFdWkFu7ArgRCIrWLh6m3O6lolnHQSCWPwtOTx893Qkp9Eu6FwUnoWc8rsYx8sqa0eNCTCS6Kjr1Iu3yEJ5y9390jUiDZTQ/c62ueCTQaYv2bwIDAQAB",
"permissions": ["downloads", "contextMenus", "storage", "nativeMessaging"], "permissions": ["downloads", "contextMenus", "storage", "nativeMessaging"],
+11
View File
@@ -90,6 +90,17 @@ else:
print("(If KGet is currently running, restart it for this to take effect.)") print("(If KGet is currently running, restart it for this to take effect.)")
PYEOF PYEOF
echo "Checking KGet's download-finished notification..."
KNOTIFYRC="$HOME/.config/knotifyrc"
if [ -f "$KNOTIFYRC" ] && grep -q '^\[Event/kget/finished\]' "$KNOTIFYRC"; then
echo "KGet's download-finished notification is already configured, leaving it as-is"
elif command -v kwriteconfig6 >/dev/null 2>&1; then
kwriteconfig6 --file knotifyrc --group "Event/kget/finished" --key Action Popup
echo "Enabled a desktop popup notification for KGet's \"Download Finished\" event"
else
echo "Skipped: 'kwriteconfig6' not found (not on KDE Plasma?) -- no popup notification set up"
fi
EXT_ID=$(cat "$SCRIPT_DIR/keys/extension-id.txt") EXT_ID=$(cat "$SCRIPT_DIR/keys/extension-id.txt")
cat <<EOF cat <<EOF
+30 -9
View File
@@ -2,11 +2,28 @@
"""Native messaging host bridging Chrome to KGet's D-Bus service.""" """Native messaging host bridging Chrome to KGet's D-Bus service."""
import json import json
import os
import struct import struct
import subprocess import subprocess
import sys import sys
def default_download_dir():
home = os.path.expanduser("~")
try:
result = subprocess.run(
["xdg-user-dir", "DOWNLOAD"], capture_output=True, text=True, timeout=5
)
downloads = result.stdout.strip()
except (FileNotFoundError, subprocess.TimeoutExpired):
downloads = ""
# xdg-user-dir falls back to bare $HOME (not $HOME/Downloads) when
# there's no ~/.config/user-dirs.dirs at all -- treat that as not found.
if not downloads or downloads.rstrip("/") == home.rstrip("/"):
return os.path.join(home, "Downloads")
return downloads
def read_message(): def read_message():
raw_length = sys.stdin.buffer.read(4) raw_length = sys.stdin.buffer.read(4)
if len(raw_length) < 4: if len(raw_length) < 4:
@@ -23,20 +40,24 @@ def write_message(message):
sys.stdout.buffer.flush() sys.stdout.buffer.flush()
def add_transfer(url): def add_transfer(url, filename):
# showNewTransferDialog opens KGet's "New Download" dialog (editable name # A non-empty destDir makes KGet add+start the transfer immediately with
# and destination) and returns immediately without waiting for the user # no interactive dialog. Passing dir+filename (when we know the real
# to interact with it. addTransfer, by contrast, only returns once the # filename, e.g. from Chrome's own Content-Disposition resolution) also
# dialog is dismissed when destDir is empty -- it blocks the D-Bus call # sidesteps KGet's own filename guess, which is derived purely from the
# on human input, which is why we don't use it here. # URL path and gets it wrong for opaque/signed URLs.
dest_dir = default_download_dir()
dest = os.path.join(dest_dir, filename) if filename else dest_dir
cmd = [ cmd = [
"dbus-send", "dbus-send",
"--print-reply", "--print-reply",
"--reply-timeout=10000", "--reply-timeout=10000",
"--dest=org.kde.kget", "--dest=org.kde.kget",
"/KGet", "/KGet",
"org.kde.kget.main.showNewTransferDialog", "org.kde.kget.main.addTransfer",
f"array:string:{url}", f"string:{url}",
f"string:{dest}",
"boolean:true",
] ]
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
@@ -57,7 +78,7 @@ def main():
write_message({"ok": False, "error": "invalid request"}) write_message({"ok": False, "error": "invalid request"})
return return
ok, error = add_transfer(message["url"]) ok, error = add_transfer(message["url"], message.get("filename"))
write_message({"ok": True} if ok else {"ok": False, "error": error}) write_message({"ok": True} if ok else {"ok": False, "error": error})