mirror of
https://github.com/andwati/kget-capture.git
synced 2026-10-18 03:02:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c395c5fbe2
|
||
|
|
64c3a55df4
|
@@ -5,12 +5,14 @@ A Chrome (and other Chromium-browser) extension that sends downloads to
|
||||
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.
|
||||
**"Download with KGet"** to send it straight to KGet — it starts
|
||||
downloading immediately into your Downloads folder, no dialog.
|
||||
- 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.
|
||||
starts is cancelled and handed off to KGet the same way, automatically,
|
||||
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
|
||||
|
||||
@@ -18,6 +20,8 @@ addition to) the browser's built-in downloader.
|
||||
- One of: Google Chrome, Chromium, Brave, Vivaldi, Microsoft Edge
|
||||
- `python3` and `dbus-send` on `PATH` (standard on virtually all Linux/KDE
|
||||
systems already)
|
||||
- `kwriteconfig6` (ships with KDE Plasma) — optional, only used to enable the
|
||||
download-finished notification; install still works without it
|
||||
|
||||
## Install
|
||||
|
||||
@@ -72,14 +76,25 @@ git push origin v1.0.0
|
||||
- **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.
|
||||
- **No editable dialog.** Captures start immediately with no chance to
|
||||
rename the file or pick a different folder beforehand — rename/move it
|
||||
afterward in KGet or your file manager if needed. This is intentional: KGet
|
||||
only skips its interactive "New Download" dialog when given a real
|
||||
destination path up front, so instant capture and an editable prompt are
|
||||
mutually exclusive given KGet's D-Bus API.
|
||||
- **The context-menu capture can't always get the correct filename.**
|
||||
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
|
||||
the user's perspective — check the service worker console for the actual
|
||||
error (see Troubleshooting).
|
||||
@@ -95,15 +110,14 @@ git push origin v1.0.0
|
||||
- **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
|
||||
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.
|
||||
- **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
|
||||
|
||||
|
||||
+30
-13
@@ -11,16 +11,20 @@ async function refreshBadge() {
|
||||
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;
|
||||
function sendToKGet(url, filename) {
|
||||
chrome.runtime.sendNativeMessage(
|
||||
NATIVE_HOST,
|
||||
{ action: "addTransfer", url, filename: filename || null },
|
||||
(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) => {
|
||||
@@ -40,10 +44,21 @@ chrome.contextMenus.onClicked.addListener((info) => {
|
||||
if (info.menuItemId !== MENU_ID) return;
|
||||
const url = info.linkUrl || info.srcUrl;
|
||||
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 });
|
||||
if (!autoCaptureEnabled) return;
|
||||
|
||||
@@ -67,8 +82,10 @@ chrome.downloads.onCreated.addListener(async (item) => {
|
||||
} catch (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) => {
|
||||
if (area === "local" && "autoCaptureEnabled" in changes) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "KGet Capture",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"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"],
|
||||
|
||||
+11
@@ -90,6 +90,17 @@ else:
|
||||
print("(If KGet is currently running, restart it for this to take effect.)")
|
||||
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")
|
||||
|
||||
cat <<EOF
|
||||
|
||||
@@ -2,11 +2,28 @@
|
||||
"""Native messaging host bridging Chrome to KGet's D-Bus service."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
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():
|
||||
raw_length = sys.stdin.buffer.read(4)
|
||||
if len(raw_length) < 4:
|
||||
@@ -23,20 +40,24 @@ def write_message(message):
|
||||
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.
|
||||
def add_transfer(url, filename):
|
||||
# A non-empty destDir makes KGet add+start the transfer immediately with
|
||||
# no interactive dialog. Passing dir+filename (when we know the real
|
||||
# filename, e.g. from Chrome's own Content-Disposition resolution) also
|
||||
# sidesteps KGet's own filename guess, which is derived purely from the
|
||||
# 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 = [
|
||||
"dbus-send",
|
||||
"--print-reply",
|
||||
"--reply-timeout=10000",
|
||||
"--dest=org.kde.kget",
|
||||
"/KGet",
|
||||
"org.kde.kget.main.showNewTransferDialog",
|
||||
f"array:string:{url}",
|
||||
"org.kde.kget.main.addTransfer",
|
||||
f"string:{url}",
|
||||
f"string:{dest}",
|
||||
"boolean:true",
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||
@@ -57,7 +78,7 @@ def main():
|
||||
write_message({"ok": False, "error": "invalid request"})
|
||||
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})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user