Add torrents to Deluge via Web API

Add torrents to Deluge via Web API (requires patched deluge-web and ViolentMonkey)

  1. // ==UserScript==
  2. // @description Add torrents to Deluge via Web API (requires patched deluge-web and ViolentMonkey)
  3. // @grant GM.xmlHttpRequest
  4. // @homepageURL https://github.com/lalbornoz/AddTorrentsDelugeTransmission
  5. // @include *
  6. // @license MIT
  7. // @name Add torrents to Deluge via Web API
  8. // @namespace https://greasyfork.org/users/467795
  9. // @supportURL https://github.com/lalbornoz/AddTorrentsDelugeTransmission
  10. // @version 1.8
  11. // ==/UserScript==
  12.  
  13. /*
  14. * Tunables
  15. */
  16. let debug = false;
  17. let delugeDownloadDir = {
  18. "": "/var/lib/deluge/downloads"
  19. };
  20. let delugeHostId = "";
  21. let delugeHttpAuthPassword = ""; // (optional)
  22. let delugeHttpAuthUsername = ""; // (optional)
  23. let delugeTorrentDirectory = "/var/lib/deluge/torrents";
  24. let delugeWebPassword = "";
  25. let delugeWebUrl = "protocol://hostname[:port]/deluge";
  26.  
  27. // {{{ Images
  28. let images = {
  29. "progress0": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAABmSURBVFhH7ZbBCcBACAQ1dVm+fSXxMJCQK2BXdl6KHwcV9PPGBrBE3L1TTmoWR8f0jBH5rVaNiYWn7+p5K5KZnX2JCKjaW0Q3goZE0JAIGhJBQyJojBGZ+zQyUiK6ETTWanVMjNkFC/lGCoFaJLkAAAAASUVORK5CYII=',
  30. "progress1": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAB2SURBVFhH7ZbBDYAwCEXBVVyDFXTYugJrOItKg4mNng2Q/0789MILkJSPCypAF2FmjzmxWUxep6eMyGu1bExZuPu2nj9F5rZ4GtnXjVTV04iI/P72FMGNRAMi0YBINCASDYhEo4xI3U9jRkwENxKNvlpeJ4boBO/6Rgr3rj+kAAAAAElFTkSuQmCC',
  31. "progress2": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAB3SURBVFhH7ZbRCYBQCEW1VVrDFWrYWsE1mqXyYdCj6C+4iudL8ceDCvJ+QgloIszsaUxsFoPH4Ukj8lgtG1MUrr6t51eRcZk869nm9bOmqp71iMgvtbtI3QgaJYJGiaBRImiUCBppRPI+jRExkboRNNpqeRwYogPUCkYKdeMSDAAAAABJRU5ErkJggg==',
  32. "progress3": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAB3SURBVFhH7ZaxDYBACEXBVVyDFXRYXYE1nEXlgokXra76EF4FoeEFSODzhhLQRJjZ05jYLCaPw5NG5LNaNqYoPH1bz78i87Z41nOs+3BNVT3rEZHh2lukbgSNEkGjRNAoETRKBI00InmfxoiYSN0IGm21PA4M0QW4GkYKEt+3hwAAAABJRU5ErkJggg==',
  33. "progress4": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAB3SURBVFhH7ZbRCYBQCEW1VVrDFWrYWsE1mqXyYdCj6C+4iudL8ceDCvJ+QgloIszsaUxsFoPH4Ukj8lgtG1MUrr6t51eRcZk869nm9ZeaqnrWIyKftbtI3QgaJYJGiaBRImiUCBppRPI+jRExkboRNNpqeRwYogOcKkYK3xiWdQAAAABJRU5ErkJggg==',
  34. "progress5": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAB0SURBVFhH7ZbBDYBACATBVmyDFrRYbYE2rEXlgg+ibwNk58XmPkyA5Pi8oQYMEWb2WBObxeR1edqIvFbLxlSFp2/r+VNk3hZPkWPdf39TVU8REQkiuJFsQCQbEMkGRLIBkWy0Een7aayIieBGsjFWy+vCEF2AOkYKkHxZgwAAAABJRU5ErkJggg==',
  35. "progress6": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAABrSURBVFhH7ZbBDYBACATBVqxHi9V6rEXlwiVezgJ2yc4LwocJkOD3ixWgibh7ppzELJaM6SkjMq1WjImF3nf0/CuyHltmI9d+QtW+IroRNCSChkTQkAgaEkGjjEjdp5GRENGNoNFWK2NizB5kSkYKKMPFzAAAAABJRU5ErkJggg==',
  36. "progresserror0": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAABsSURBVFhH7daxDcAgDERROyOkT8n+A6WkzwoJRk4RkQHurHsVdP4CJPwerIAZ4u655RRnseWaXpmQ5WrFMbF4546Zf0Ouo+UO197PT4jeCBqFoFEIGoWgUQiaMiF1P42MIkRvBM28WrkmZvYAO4w3Clt9ChsAAAAASUVORK5CYII=',
  37. "progresserror1": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAB3SURBVFhH7ZaxDYBACEXBEewt3YLx2cLS3hVULlziRWvzIbyKn2t4AZLj84YS0ESY2WNMbBaT1+FJI/JaLRtTFHrf1vOnyLGsnkbmfSNV9TQiIr+/PUXqRtAoETRKBI0SQaNE0EgjkvfTGBETqRtBo62W14EhugDxSkYK6vuriAAAAABJRU5ErkJggg==',
  38. "progresserror2": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAB4SURBVFhH7ZYxCoBQCIa1I7Q3dguP7y0a27tC5cOgR9EWqPzfpLj4oYK8n1ABmggze5oTm8XgcXrKiDxWy8aUhatv6/lVZJtmz3rGdfmsqapnPSLyS+0ughuJBkSiAZFoQCQaEIlGGZG6T2NGTAQ3Eo22Wh4nhugA1qpGCkK2tScAAAAASUVORK5CYII=',
  39. "progresserror3": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAB3SURBVFhH7ZaxDYBACEXBEewt3YLx2cLS3hVULph40eoqIP9VEBpegAQ+b6gATYSZPc2JzWLyOD1lRD6rZWPKwtO39fwrciyrZz3zvg3XVNWzHhEZrr1FcCPRgEg0IBINiEQDItEoI1L3acyIieBGotFWy+PEEF28CkYKuDE5FAAAAABJRU5ErkJggg==',
  40. "progresserror4": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAB4SURBVFhH7ZYxCoBQCIa1I7Q3dguP7y0a27tC5cOgR9EWqPzfpLj4oYK8n1ABmggze5oTm8XgcXrKiDxWy8aUhatv6/lVZJtmz3rGdfmlpqqe9YjIZ+0ughuJBkSiAZFoQCQaEIlGGZG6T2NGTAQ3Eo22Wh4nhugAoWpGCo2Q1WAAAAAASUVORK5CYII=',
  41. "progresserror5": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAB1SURBVFhH7ZaxDYBACEXBEewt3YLx2cLS3hVULlgQrQ2Q/yp+ruEFSI7PG2rAEGFmjzWxWUxel6eNyGu1bExVePq2nj9FjmX1FJn37fc3VfUUEZEgghvJBkSyAZFsQCQbEMlGG5G+n8aKmAhuJBtjtbwuDNEFhspGCibKMf8AAAAASUVORK5CYII=',
  42. "progresserror6": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAWCAYAAACCAs+RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAABtSURBVFhH7ZYxDoBACATBJ9hb+v8HWdr7BZULl3g5H7BLdioIDRMgwe8XK0ATcfdMOYlZLBnTU0ZkWq0YEwu97+j5V+Ta9sxG1vOAqn1FdCNoSAQNiaAhETQkgkYZkbpPIyMhohtBo61WxsSYPWwqRgrWtBo1AAAAAElFTkSuQmCC',
  43. };
  44. // }}}
  45. // {{{ Module variables
  46. let delugeRequestId = 0;
  47. // }}}
  48.  
  49. // {{{ function basename(url)
  50. function basename(url) {
  51. let url_ = url.split("/");
  52. return url_[url_.length - 1];
  53. };
  54. // }}}
  55. // {{{ function btoa2(data)
  56. function btoa2(data) {
  57. return btoa(new Uint8Array(data).reduce(
  58. function(data, byte) {
  59. return data + String.fromCharCode(byte);
  60. }, ""));
  61. };
  62. // }}}
  63. // {{{ function decodeURI2(uri)
  64. function decodeURI2(uri) {
  65. return decodeURI(uri).replace(/\+/g, " ");
  66. };
  67. // }}}
  68. // {{{ function deletePrototypeFunctions()
  69. function deletePrototypeFunctions() {
  70. if(window.Prototype) {
  71. logDebug("Prototype.js detected, deleting possibly broken {Object,Array,Hash,String}.prototype.toJSON() functions");
  72. delete Object.prototype.toJSON;
  73. delete Array.prototype.toJSON;
  74. delete Hash.prototype.toJSON;
  75. delete String.prototype.toJSON;
  76. };
  77. };
  78. // }}}
  79. // {{{ function isMagnetLink(url)
  80. function isMagnetLink(url) {
  81. if (url.match(/^magnet:/i)) {
  82. return true;
  83. } else {
  84. return false;
  85. };
  86. };
  87. // }}}
  88. // {{{ function isTorrentLink(url)
  89. function isTorrentLink(url) {
  90. if (url.match(/\.torrent(\?.*|)$/i)) {
  91. return true;
  92. } else {
  93. return false;
  94. };
  95. };
  96. // }}}
  97. // {{{ function matchHostDict(dict, host)
  98. function matchHostDict(dict, host) {
  99. let hostDomain = host.split(".").slice(-2);
  100. if (host in dict) {
  101. return dict[host];
  102. } else if (hostDomain in dict) {
  103. return dict[hostDomain];
  104. } else {
  105. return dict[""];
  106. };
  107. };
  108. // }}}
  109.  
  110. // {{{ function delugeWebRequest(method, onLoadCb, params)
  111. function delugeWebRequest(method, onLoadCb, params) {
  112. let headers = {"Content-type": "application/json"};
  113. let paramsJson = JSON.stringify(params);
  114. let xhrParams = {
  115. anonymous: false,
  116. data: '\{"method":"' + method + '","params":' + paramsJson + ',"id":' + (delugeRequestId++) + '\}',
  117. headers: headers,
  118. method: "POST",
  119. onload: function (xhr) {
  120. let response = null;
  121. try {
  122. response = JSON.parse(xhr.responseText);
  123. }
  124. catch (error) {
  125. logError("Error parsing response from server as JSON: "
  126. + xhr.responseText);
  127. };
  128. if (response.error === null) {
  129. logDebug("Asynchronous `" + method
  130. + "' Web API request succeeded w/ response="
  131. + JSON.stringify(response));
  132. } else {
  133. logDebug("Asynchronous `" + method
  134. + "' Web API request failed: " + response.error.message
  135. + " (code " + response.error.code.toString() + ")");
  136. };
  137. onLoadCb(response, xhr);
  138. },
  139. synchronous: false,
  140. url: delugeWebUrl + "/json"
  141. };
  142. if ((delugeHttpAuthPassword !== "")
  143. && (delugeHttpAuthUsername !== "")) {
  144. xhrParams["password"] = delugeHttpAuthPassword;
  145. xhrParams["user"] = delugeHttpAuthUsername;
  146. };
  147. logDebug("POSTing asynchronous `" + method + "' Web API request to " + xhrParams["url"]
  148. + " (JSON-encoded parameters: " + paramsJson + ")");
  149. GM.xmlHttpRequest(xhrParams);
  150. };
  151. // }}}
  152. // {{{ function logDebug(msg)
  153. function logDebug(msg) {
  154. if (debug) {
  155. console.log("[Deluge] " + msg);
  156. };
  157. };
  158. // }}}
  159. // {{{ function logError(msg)
  160. function logError(msg) {
  161. logDebug(msg);
  162. alert("[Deluge] " + msg);
  163. };
  164. // }}}
  165. // {{{ function logInfo(msg)
  166. function logInfo(msg) {
  167. logDebug(msg);
  168. alert("[Deluge] " + msg);
  169. };
  170. // }}}
  171. // {{{ function registerLink(cb, link, linkImage, linkNewId)
  172. function registerLink(cb, link, linkImage, linkNewId) {
  173. let linkNew = document.createElement("a");
  174. linkNew.innerHTML = '<img src="' + linkImage + '" style="border: 0px" />';
  175. linkNew.setAttribute("href", link.href);
  176. linkNew.setAttribute("id", linkNewId);
  177. linkNew.style.paddingLeft = "2px";
  178. link.parentNode.insertBefore(linkNew, link.nextSibling);
  179. linkNew.addEventListener("click", cb, true);
  180. };
  181. // }}}
  182. // {{{ function setLinkState(link, linkState, msg, error=false)
  183. function setLinkState(link, linkState, msg, error=false) {
  184. if (error === false) {
  185. link.title = msg; link.src = images["progress" + linkState.toString()]; logDebug(msg);
  186. } else {
  187. link.title = msg; link.src = images["progresserror" + linkState.toString()]; logError(msg);
  188. };
  189. };
  190. // }}}
  191.  
  192. // {{{ function cbClickMagnet(e)
  193. function cbClickMagnet(e) {
  194. let torrentUrl = this.href, torrentName_ = torrentUrl.match(/dn=([^&]+)/);
  195. e.stopPropagation(); e.preventDefault();
  196. if (torrentName_ === null) {
  197. setLinkState(e.target, 0, "Invalid Magnet URI (missing Display Name)", true);
  198. } else {
  199. torrentName = decodeURI2(torrentName_[1]);
  200. setLinkState(e.target, 2, "Logging into Deluge Web server...");
  201. delugeWebRequest("auth.login",
  202. function (response, xhr_) {
  203. cbWebLoginResponse(e.target, response, null, delugeDownloadDir[""],
  204. torrentName, torrentUrl, null, xhr_);
  205. }, [delugeWebPassword]);
  206. };
  207. };
  208. // }}}
  209. // {{{ function cbClickTorrent(e)
  210. function cbClickTorrent(e) {
  211. let torrentUrl = this.href, torrentUrlHost_ = torrentUrl.match(new RegExp("^[^:]+://(?:[^:]+:[^@]+@)?([^/:]+)"));
  212. e.stopPropagation(); e.preventDefault();
  213. if (torrentUrlHost_ === null) {
  214. setLinkState(e.target, 0, "Failed to obtain hostname from BitTorrent URL", true);
  215. } else {
  216. let torrentDownloadDir = "", torrentName = basename(torrentUrl), torrentUrlHost = torrentUrlHost_[1];
  217. if ((torrentDownloadDir = matchHostDict(delugeDownloadDir, torrentUrlHost)) === null) {
  218. torrentDownloadDir = delugeDownloadDir[""];
  219. };
  220. setLinkState(e.target, 1, "Sending asynchronous GET request for " + torrentUrl + "...");
  221. GM.xmlHttpRequest({
  222. method: "GET",
  223. onreadystatechange: function (xhr) {
  224. cbClickTorrentResponse(e.target, xhr.response, torrentDownloadDir,
  225. torrentName, torrentUrl, torrentUrlHost, xhr);
  226. },
  227. responseType: "arraybuffer",
  228. synchronous: false,
  229. url: torrentUrl
  230. });
  231. };
  232. };
  233. // }}}
  234. // {{{ function cbClickTorrentResponse(link, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr)
  235. function cbClickTorrentResponse(link, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr) {
  236. logDebug("Asynchronous GET request for " + torrentUrl
  237. + " readyState=" + xhr.readyState + " status=" + xhr.status);
  238. if (xhr.readyState === 4) {
  239. if (xhr.status === 200) {
  240. setLinkState(link, 2, "Received torrent data, logging into Deluge Web server...");
  241. delugeWebRequest("auth.login",
  242. function (response, xhr_) {
  243. cbWebLoginResponse(link, response, torrent, torrentDownloadDir,
  244. torrentName, torrentUrl, torrentUrlHost, xhr_);
  245. }, [delugeWebPassword]);
  246. } else {
  247. setLinkState(link, 2, "Asynchronous GET request for " + torrentUrl + " failed w/ status=" + xhr.status, false);
  248. };
  249. };
  250. };
  251. // }}}
  252. // {{{ function cbWebLoginResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr)
  253. function cbWebLoginResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr) {
  254. if (response.error === null) {
  255. setLinkState(link, 3, "Logged into Deluge Web server, sending web.connect request...");
  256. delugeWebRequest("web.connect",
  257. function (response_, xhr_) {
  258. cbWebConnectResponse(link, response_, torrent, torrentDownloadDir,
  259. torrentName, torrentUrl, xhr_);
  260. }, [delugeHostId]);
  261. } else {
  262. setLinkState(link, 3,
  263. "web.login request failed: " + response.error.message
  264. + " (code=" + response.error.code.toString() + ")", true);
  265. };
  266. };
  267. // }}}
  268. // {{{ function cbWebConnectResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr)
  269. function cbWebConnectResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr) {
  270. if (response.error === null) {
  271. setLinkState(link, 4, "Connected to Deluge Web server, sending web.get_config request...");
  272. delugeWebRequest("web.get_config",
  273. function (response_, xhr_) {
  274. cbWebGetConfigResponse(link, response_, torrent, torrentDownloadDir,
  275. torrentName, torrentUrl, xhr_);
  276. }, []);
  277. } else {
  278. setLinkState(link, 4,
  279. "web.connect request failed: " + response.error.message
  280. + " (code=" + response.error.code.toString() + ")", true);
  281. };
  282. };
  283. // }}}
  284. // {{{ function cbWebGetConfigResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr)
  285. function cbWebGetConfigResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr) {
  286. if (response.error === null) {
  287. let params = [{options: {"download_location": torrentDownloadDir}}];
  288. if (isMagnetLink(torrentUrl)) {
  289. params[0]["path"] = torrentUrl;
  290. } else {
  291. params[0]["data"] = btoa2(torrent);
  292. params[0]["path"] = delugeTorrentDirectory + "/" + torrentName;
  293. };
  294. setLinkState(link, 5, "Received web.get_config response, sending web.add_torrents request...");
  295. delugeWebRequest("web.add_torrents",
  296. function (response_, xhr_) {
  297. cbWebAddTorrentsResponse(link, response_, torrent, torrentDownloadDir,
  298. torrentName, torrentUrl, torrentUrlHost, xhr_);
  299. }, [params]);
  300. } else {
  301. setLinkState(link, 5,
  302. "web.get_config request failed: " + response.error.message
  303. + " (code=" + response.error.code.toString() + ")", true);
  304. };
  305. };
  306. // }}}
  307. // {{{ function cbWebAddTorrentsResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr)
  308. function cbWebAddTorrentsResponse(link, response, torrent, torrentDownloadDir, torrentName, torrentUrl, torrentUrlHost, xhr) {
  309. if (response.error === null) {
  310. setLinkState(link, 6, "Successfully added torrent");
  311. logInfo("Torrent `" + torrentName + "' added successfully.");
  312. } else {
  313. setLinkState(link, 6,
  314. "web.add_torrents request failed: " + response.error.message
  315. + " (code=" + response.error.code.toString() + ")", true);
  316. };
  317. };
  318. // }}}
  319.  
  320. function main() {
  321. logDebug("Entry point");
  322. deletePrototypeFunctions();
  323. for (let link of document.links) {
  324. if (link.getAttribute("id") === "AddTorrentsDelugeLink") {
  325. continue;
  326. } else if (isMagnetLink(link.href)) {
  327. registerLink(cbClickMagnet, link, images["progress0"], "AddTorrentsDelugeLink");
  328. logDebug("Registered Magnet link " + link.href);
  329. } else if (isTorrentLink(link.href)) {
  330. registerLink(cbClickTorrent, link, images["progress0"], "AddTorrentsDelugeLink");
  331. logDebug("Registered BitTorrent link " + link.href);
  332. };
  333. };
  334. };
  335.  
  336. main();
  337.  
  338. // vim:expandtab fileformat=dos sw=2 ts=2