Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/webui/www/private/addtorrent.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"use strict";

window.addEventListener("DOMContentLoaded", (event) => {
const searchParams = new URLSearchParams(window.location.search);

let submitted = false;

window.addEventListener("keydown", (event) => {
switch (event.key) {
case "Escape":
Expand All @@ -34,7 +38,37 @@
}
});

const searchParams = new URLSearchParams(window.location.search);
const getClipboardText = (clipboardData) => {
for (const type of ["text/plain", "text/uri-list", "text"]) {
const text = clipboardData?.getData(type);
if ((typeof text === "string") && (text.length > 0))
return text;
}
return "";
};

const pasteTorrentLinks = (event) => {
const text = getClipboardText(event.clipboardData);
if (typeof text !== "string")
return false;

const urls = window.qBittorrent.Misc.torrentLinksFromText(text);
if (urls.length === 0)
return false;

event.preventDefault();
event.stopImmediatePropagation();

document.getElementById("urls").value = urls.join("\n");
submitted = true;
window.qBittorrent.AddTorrent.submitForm();
document.getElementById("uploadForm").submit();
return true;
};

document.addEventListener("paste", (event) => {
pasteTorrentLinks(event);
}, true);

const source = searchParams.get("source");
if ((source === null) || (source.length === 0))
Expand All @@ -44,7 +78,6 @@

// fetch unless explicitly told not to
const fetchMetadata = searchParams.get("fetch") !== "false";
let submitted = false;

const windowId = searchParams.get("windowId");
window.qBittorrent.AddTorrent.setWindowId(windowId);
Expand Down
61 changes: 51 additions & 10 deletions src/webui/www/private/scripts/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ window.qBittorrent.Client ??= (() => {
isShowRssReader: isShowRssReader,
isShowLogViewer: isShowLogViewer,
createAddTorrentWindow: createAddTorrentWindow,
pasteTorrentLinks: pasteTorrentLinks,
uploadTorrentFiles: uploadTorrentFiles,
categoryMap: categoryMap,
tagMap: tagMap
Expand All @@ -56,6 +57,51 @@ window.qBittorrent.Client ??= (() => {
// Map<tag: String, torrents: Set>
const tagMap = new Map();

const addTorrentUrls = async (urls) => {
if (urls.length === 0)
return false;

try {
const response = await fetch("api/v2/torrents/add", {
method: "POST",
body: new URLSearchParams({
urls: urls.join("\n")
})
});

if (response.status === 409)
return true;
return response.ok;
}
catch {
return false;
}
};

const getClipboardText = (clipboardData) => {
for (const type of ["text/plain", "text/uri-list", "text"]) {
const text = clipboardData?.getData(type);
if ((typeof text === "string") && (text.length > 0))
return text;
}
return "";
};

const pasteTorrentLinks = async (event) => {
const text = getClipboardText(event.clipboardData);
if (typeof text !== "string")
return false;

const urls = window.qBittorrent.Misc.torrentLinksFromText(text);
if (urls.length === 0)
return false;

event.preventDefault();
event.stopImmediatePropagation();
await addTorrentUrls(urls);
return true;
};

let cacheAllSettled;
let clientDataPromise;
const setup = () => {
Expand Down Expand Up @@ -1842,16 +1888,7 @@ window.addEventListener("DOMContentLoaded", async (event) => {
if (droppedText.length > 0) {
// dropped text

const urls = droppedText.split("\n")
.map((str) => str.trim())
.filter((str) => {
const lowercaseStr = str.toLowerCase();
return lowercaseStr.startsWith("http:")
|| lowercaseStr.startsWith("https:")
|| lowercaseStr.startsWith("magnet:")
|| ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
|| ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
});
const urls = window.qBittorrent.Misc.torrentLinksFromText(droppedText);

for (const url of urls)
qBittorrent.Client.createAddTorrentWindow(url, url);
Expand All @@ -1860,6 +1897,10 @@ window.addEventListener("DOMContentLoaded", async (event) => {
};
registerDragAndDrop();

document.addEventListener("paste", async (event) => {
await window.qBittorrent.Client.pasteTorrentLinks(event);
}, true);

window.addEventListener("keydown", (event) => {
switch (event.key) {
case "a":
Expand Down
45 changes: 45 additions & 0 deletions src/webui/www/private/scripts/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ window.qBittorrent.Misc ??= (() => {
getHost: getHost,
createDebounceHandler: createDebounceHandler,
filterInPlace: filterInPlace,
isTorrentLink: isTorrentLink,
torrentLinksFromText: torrentLinksFromText,
friendlyUnit: friendlyUnit,
friendlyDuration: friendlyDuration,
friendlyPercentage: friendlyPercentage,
Expand Down Expand Up @@ -195,6 +197,49 @@ window.qBittorrent.Misc ??= (() => {
return text.replace(exp, "<a target='_blank' rel='noopener noreferrer' href='$1'>$1</a>");
};

const isTorrentLink = (str) => {
const lowercase = str.toLowerCase();
return lowercase.startsWith("http:")
|| lowercase.startsWith("https:")
|| lowercase.startsWith("magnet:")
|| ((str.length === 40) && !(/[^0-9A-F]/i.test(str))) // v1 hex-encoded SHA-1 info-hash
|| ((str.length === 32) && !(/[^2-7A-Z]/i.test(str))); // v1 Base32 encoded SHA-1 info-hash
};

const normalizeTorrentLink = (str) => {
const trimmed = safeTrim(str);
if (trimmed.length < 2)
return trimmed;

const firstChar = trimmed[0];
const lastChar = trimmed[trimmed.length - 1];
if (((firstChar === "'") && (lastChar === "'"))
|| ((firstChar === "\"") && (lastChar === "\""))
|| ((firstChar === "<") && (lastChar === ">")))
return trimmed.slice(1, -1).trim();

return trimmed;
};

const torrentLinksFromText = (text) => {
const urls = [];
const seen = new Set();

for (const line of text.split("\n")) {
const url = normalizeTorrentLink(line);
if (url.length === 0)
continue;

if (!isTorrentLink(url) || seen.has(url))
continue;

seen.add(url);
urls.push(url);
}

return urls;
};

/**
* Parse a string into a Version Record.
* It is generally recommended to use `compareVersions()` instead.
Expand Down
22 changes: 22 additions & 0 deletions src/webui/www/test/private/misc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,25 @@ test("Test formatDate() - Fallback Behavior", () => {
// Restore original window.parent
window.parent = originalParent;
});

test("Test torrentLinksFromText()", () => {
const { isTorrentLink, torrentLinksFromText } = window.qBittorrent.Misc;

expect(isTorrentLink("magnet:?xt=urn:btih:abc")).toBe(true);
expect(isTorrentLink("https://example.com/file.torrent")).toBe(true);
expect(isTorrentLink("not a torrent link")).toBe(false);

expect(torrentLinksFromText([
" magnet:?xt=urn:btih:abc ",
"\"magnet:?xt=urn:btih:abc\"",
"'magnet:?xt=urn:btih:abc'",
"<magnet:?xt=urn:btih:abc>",
"",
"https://example.com/file.torrent",
"not a torrent link",
"magnet:?xt=urn:btih:abc"
].join("\n"))).toStrictEqual([
"magnet:?xt=urn:btih:abc",
"https://example.com/file.torrent"
]);
});
Loading