mirror of
https://github.com/andwati/kget-capture.git
synced 2026-10-31 17:22:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235c377361
|
||
|
|
c395c5fbe2
|
||
|
|
64c3a55df4
|
@@ -5,12 +5,17 @@ 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.
|
||||||
|
- A "Download complete" desktop notification appears once a capture actually
|
||||||
|
finishes downloading. This is driven by the extension itself (not KGet's
|
||||||
|
own per-transfer notification, which KGet silently skips for downloads
|
||||||
|
that finish very quickly — see Troubleshooting), so it fires reliably
|
||||||
|
regardless of file size or speed.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -18,6 +23,12 @@ 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)
|
||||||
|
- `notify-send` (ships with most Linux desktops via libnotify) — optional,
|
||||||
|
only used for the download-complete notification; captures still work
|
||||||
|
without it, just silently
|
||||||
|
- `kwriteconfig6` (ships with KDE Plasma) — optional, only used to disable
|
||||||
|
KGet's own unreliable per-transfer notification so it doesn't double up
|
||||||
|
with the extension's; install still works without it
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -34,6 +45,12 @@ finds installed, then prints the extension's fixed ID and next steps:
|
|||||||
2. Enable **Developer mode** (top-right toggle).
|
2. Enable **Developer mode** (top-right toggle).
|
||||||
3. Click **Load unpacked** and select this repo's `extension/` directory.
|
3. Click **Load unpacked** and select this repo's `extension/` directory.
|
||||||
4. Confirm the loaded extension's ID matches the one `install.sh` printed.
|
4. Confirm the loaded extension's ID matches the one `install.sh` printed.
|
||||||
|
5. Open `chrome://settings/downloads` (or your browser's equivalent, e.g.
|
||||||
|
`brave://settings/downloads`, `edge://settings/downloads`) and turn **off**
|
||||||
|
"Ask where to save each file before downloading". Chrome shows this dialog
|
||||||
|
itself, before the extension ever gets a chance to intercept the download —
|
||||||
|
no extension API can read or suppress it, so this one-time setting change
|
||||||
|
is required for captures to work without an extra prompt.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -72,14 +89,21 @@ 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** if you skipped step 5 of
|
||||||
|
Install above — see there for why and how to fix it.
|
||||||
- **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 +119,22 @@ 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-complete notification not showing?** Confirm `notify-send` is
|
||||||
|
installed (`command -v notify-send`) — without it, `kget_capture_host.py`
|
||||||
|
skips the notification entirely rather than erroring. Otherwise, check that
|
||||||
|
the transfer actually reached 100%: `kget_capture_host.py` polls
|
||||||
|
`org.kde.kget.transfer.percent` on the new transfer's D-Bus object path
|
||||||
|
(e.g. `/KGet/Transfers/9`, printed by the `addTransfer` call above) until it
|
||||||
|
hits 100, so a stuck/erroring transfer never fires one. Note this
|
||||||
|
notification is separate from KGet's own built-in "Download Finished"
|
||||||
|
event — `install.sh` disables that one (`kwriteconfig6 --file knotifyrc
|
||||||
|
--group "Event/kget/finished" --key Action None`) because KGet only fires
|
||||||
|
it for transfers it observed in a non-finished state at least once, so it
|
||||||
|
silently skips small/fast downloads that complete before that first
|
||||||
|
observation.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+30
-13
@@ -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,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "KGet Capture",
|
"name": "KGet Capture",
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"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"],
|
||||||
|
|||||||
@@ -27,3 +27,12 @@ h1 {
|
|||||||
color: #777;
|
color: #777;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint button {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@
|
|||||||
<p class="status" id="status-line"></p>
|
<p class="status" id="status-line"></p>
|
||||||
<p class="hint">Right-click any link, image, video, or audio and choose
|
<p class="hint">Right-click any link, image, video, or audio and choose
|
||||||
"Download with KGet" to send it to KGet on demand.</p>
|
"Download with KGet" to send it to KGet on demand.</p>
|
||||||
|
<p class="hint">Downloads still prompting for a location? Chrome shows that
|
||||||
|
dialog itself, before this extension can intercept anything.
|
||||||
|
<button id="open-download-settings" type="button">Turn off Chrome's download prompt</button>
|
||||||
|
</p>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ checkbox.addEventListener("change", () => {
|
|||||||
chrome.storage.local.set({ autoCaptureEnabled: checkbox.checked });
|
chrome.storage.local.set({ autoCaptureEnabled: checkbox.checked });
|
||||||
renderStatus(checkbox.checked);
|
renderStatus(checkbox.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById("open-download-settings").addEventListener("click", () => {
|
||||||
|
chrome.tabs.create({ url: "chrome://settings/downloads" });
|
||||||
|
});
|
||||||
|
|||||||
+51
@@ -51,6 +51,24 @@ if [ "$registered_any" = false ]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
declare -A BROWSER_SETTINGS_URLS=(
|
||||||
|
["Google Chrome"]="chrome://settings/downloads"
|
||||||
|
["Chromium"]="chrome://settings/downloads"
|
||||||
|
["Brave"]="brave://settings/downloads"
|
||||||
|
["Vivaldi"]="vivaldi://settings/downloads"
|
||||||
|
["Microsoft Edge"]="edge://settings/downloads"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Checking Chrome's \"Ask where to save each file\" setting..."
|
||||||
|
prompt_warnings=()
|
||||||
|
for browser in "${!BROWSER_DIRS[@]}"; do
|
||||||
|
config_dir="${BROWSER_DIRS[$browser]}"
|
||||||
|
prefs_file="$config_dir/Default/Preferences"
|
||||||
|
if [ -f "$prefs_file" ] && grep -q '"prompt_for_download":true' "$prefs_file" 2>/dev/null; then
|
||||||
|
prompt_warnings+=(" - $browser: open ${BROWSER_SETTINGS_URLS[$browser]} and turn off \"Ask where to save each file before downloading\"")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo "Checking KGet's default download folder..."
|
echo "Checking KGet's default download folder..."
|
||||||
python3 - <<'PYEOF'
|
python3 - <<'PYEOF'
|
||||||
import configparser
|
import configparser
|
||||||
@@ -90,6 +108,27 @@ 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 "Disabling KGet's own per-transfer notification..."
|
||||||
|
# KGet only fires this event if the transfer was still non-Finished the
|
||||||
|
# first time its UI observed it -- small/fast downloads routinely finish
|
||||||
|
# before that, so KGet silently skips it for them. kget_capture_host.py
|
||||||
|
# shows its own "Download complete" notification (via notify-send) for
|
||||||
|
# every capture instead, driven by polling actual transfer progress, so it
|
||||||
|
# fires reliably regardless of download speed. Disable KGet's copy here to
|
||||||
|
# avoid a duplicate popup on the transfers that are slow enough for it to
|
||||||
|
# fire on its own.
|
||||||
|
if command -v kwriteconfig6 >/dev/null 2>&1; then
|
||||||
|
kwriteconfig6 --file knotifyrc --group "Event/kget/finished" --key Action None
|
||||||
|
echo "Done -- KGet Capture's own notification will be used instead"
|
||||||
|
else
|
||||||
|
echo "Skipped: 'kwriteconfig6' not found (not on KDE Plasma?) -- KGet's own notification, if any, is left as configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for 'notify-send' (used for the download-complete notification)..."
|
||||||
|
if ! command -v notify-send >/dev/null 2>&1; then
|
||||||
|
echo "WARNING: 'notify-send' not found -- captures will still work, but you won't get a notification when they finish"
|
||||||
|
fi
|
||||||
|
|
||||||
EXT_ID=$(cat "$SCRIPT_DIR/keys/extension-id.txt")
|
EXT_ID=$(cat "$SCRIPT_DIR/keys/extension-id.txt")
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -106,3 +145,15 @@ Next steps:
|
|||||||
|
|
||||||
Make sure KGet is running (or set to autostart) for captures to work.
|
Make sure KGet is running (or set to autostart) for captures to work.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
if [ "${#prompt_warnings[@]}" -gt 0 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
WARNING: "Ask where to save each file before downloading" is ON for:
|
||||||
|
$(printf '%s\n' "${prompt_warnings[@]}")
|
||||||
|
|
||||||
|
Chrome shows that dialog itself, before the extension ever gets a chance to
|
||||||
|
intercept the download -- no extension API can read or suppress it. Turn it
|
||||||
|
off in each browser above or captures will keep prompting for a location.
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|||||||
@@ -2,11 +2,30 @@
|
|||||||
"""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 re
|
||||||
|
import shutil
|
||||||
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,32 +42,109 @@ 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)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return False, "dbus-send not found on this system"
|
return False, "dbus-send not found on this system", None, dest
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return False, "dbus-send call timed out (is KGet running?)"
|
return False, "dbus-send call timed out (is KGet running?)", None, dest
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
stderr = result.stderr.strip()
|
stderr = result.stderr.strip()
|
||||||
return False, f"dbus-send failed: {stderr or 'unknown error'}"
|
return False, f"dbus-send failed: {stderr or 'unknown error'}", None, dest
|
||||||
return True, None
|
|
||||||
|
# addTransfer replies with an array of the new transfer's D-Bus object
|
||||||
|
# path(s), e.g. `array [ string "/KGet/Transfers/9" ]` -- grab the first
|
||||||
|
# one so we can poll it for completion (see spawn_notifier).
|
||||||
|
match = re.search(r'string "(/KGet/Transfers/\d+)"', result.stdout)
|
||||||
|
transfer_path = match.group(1) if match else None
|
||||||
|
return True, None, transfer_path, dest
|
||||||
|
|
||||||
|
|
||||||
|
# KGet only fires its own "Download Finished" desktop notification if the
|
||||||
|
# transfer was still in a non-Finished state the first time KGet's UI
|
||||||
|
# observed it (see GenericObserver::transfersChangedEvent in KGet's
|
||||||
|
# core/kget.cpp: `status() == Finished && startStatus() != Finished`).
|
||||||
|
# Small/fast downloads routinely finish before that first observation, so
|
||||||
|
# KGet silently skips the notification for them -- confirmed by tracing
|
||||||
|
# actual D-Bus traffic against a running KGet instance. We replace it with
|
||||||
|
# our own, driven by polling the transfer's real progress, so captures
|
||||||
|
# always get a completion notification regardless of how fast they finish.
|
||||||
|
_NOTIFIER_SCRIPT = """
|
||||||
|
import subprocess, sys, time
|
||||||
|
|
||||||
|
path, dest = sys.argv[1], sys.argv[2]
|
||||||
|
|
||||||
|
def get_percent():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["dbus-send", "--session", "--print-reply", "--dest=org.kde.kget",
|
||||||
|
path, "org.kde.kget.transfer.percent"],
|
||||||
|
capture_output=True, text=True, timeout=10,
|
||||||
|
)
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
return None
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("int32"):
|
||||||
|
return int(line.split()[-1])
|
||||||
|
return None
|
||||||
|
|
||||||
|
deadline = time.time() + 6 * 3600
|
||||||
|
finished = False
|
||||||
|
while time.time() < deadline:
|
||||||
|
percent = get_percent()
|
||||||
|
if percent is None:
|
||||||
|
break # transfer object is gone: finished+cleared, cancelled, or KGet restarted
|
||||||
|
if percent >= 100:
|
||||||
|
finished = True
|
||||||
|
break
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
if finished:
|
||||||
|
subprocess.run(
|
||||||
|
["notify-send", "--app-name=KGet Capture", "--icon=kget",
|
||||||
|
"Download complete", dest.rsplit("/", 1)[-1]],
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def spawn_notifier(transfer_path, dest):
|
||||||
|
if not transfer_path or not shutil.which("notify-send"):
|
||||||
|
return
|
||||||
|
# Detached (new session, no inherited pipes) so it keeps running after
|
||||||
|
# Chrome closes the native-messaging connection and kills this process --
|
||||||
|
# sendNativeMessage() tears down its native host as soon as it gets a
|
||||||
|
# reply, which is long before a real download finishes.
|
||||||
|
subprocess.Popen(
|
||||||
|
[sys.executable, "-c", _NOTIFIER_SCRIPT, transfer_path, dest],
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -57,7 +153,9 @@ 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, transfer_path, dest = add_transfer(message["url"], message.get("filename"))
|
||||||
|
if ok:
|
||||||
|
spawn_notifier(transfer_path, dest)
|
||||||
write_message({"ok": True} if ok else {"ok": False, "error": error})
|
write_message({"ok": True} if ok else {"ok": False, "error": error})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user