mirror of
https://github.com/transmission/transmission.git
synced 2025-12-24 20:35:36 +00:00
feat: new JSON-RPC 2.0 RPC API (#7269)
* feat: add enum for JSON-RPC error codes * feat: new `tr_rpc_request_exec()` overload that accepts string * feat: add JSON-RPC parse error handling * feat: add logic for branching to JSON-RPC or legacy API * feat: error codes for existing errors strings * refactor: async handlers now take the done cb as parameter * feat: support non-batch JSON-RPC requests * feat: support batch JSON-RPC requests * refactor: move JSON-RPC error codes to header * test: new tests for JSON-RPC * refactor(webui): use jsonrpc api * docs: update docs for jsonrpc * fix: clang-tidy warning * perf: avoid copying callback in batch mode * code review: don't commit to dropping old RPC * chore: fix shadowed variable warnings
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
import { AlertDialog } from './alert-dialog.js';
|
||||
import { Formatter } from './formatter.js';
|
||||
import { RPC } from './remote.js';
|
||||
import { createDialogContainer, makeUUID } from './utils.js';
|
||||
|
||||
const is_ios =
|
||||
@@ -76,20 +77,24 @@ export class OpenDialog extends EventTarget {
|
||||
return;
|
||||
}
|
||||
const o = {
|
||||
arguments: {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-add',
|
||||
params: {
|
||||
'download-dir': destination,
|
||||
metainfo: contents.slice(Math.max(0, index + key.length)),
|
||||
paused,
|
||||
},
|
||||
method: 'torrent-add',
|
||||
};
|
||||
remote.sendRequest(o, (response) => {
|
||||
if (response.result !== 'success') {
|
||||
alert(`Error adding "${file.name}": ${response.result}`);
|
||||
if ('error' in response) {
|
||||
const message =
|
||||
response.error.data?.errorString ?? response.error.message;
|
||||
alert(`Error adding "${file.name}": ${message}`);
|
||||
controller.setCurrentPopup(
|
||||
new AlertDialog({
|
||||
heading: `Error adding "${file.name}"`,
|
||||
message: response.result,
|
||||
message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -104,19 +109,21 @@ export class OpenDialog extends EventTarget {
|
||||
url = `magnet:?xt=urn:btih:${url}`;
|
||||
}
|
||||
const o = {
|
||||
arguments: {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-add',
|
||||
params: {
|
||||
'download-dir': destination,
|
||||
filename: url,
|
||||
paused,
|
||||
},
|
||||
method: 'torrent-add',
|
||||
};
|
||||
remote.sendRequest(o, (payload) => {
|
||||
if (payload.result !== 'success') {
|
||||
if ('error' in payload) {
|
||||
controller.setCurrentPopup(
|
||||
new AlertDialog({
|
||||
heading: `Error adding "${url}"`,
|
||||
message: payload.result,
|
||||
message: payload.error.data?.errorString ?? payload.error.message,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,10 +61,11 @@ export class PrefsDialog extends EventTarget {
|
||||
return;
|
||||
}
|
||||
|
||||
const args = response.result ?? response.error?.data ?? {};
|
||||
const element = this.elements.network.port_status_label[ip_protocol];
|
||||
const is_open = response.arguments['port-is-open'] || false;
|
||||
const is_open = args['port-is-open'] ?? false;
|
||||
element.dataset.open = is_open;
|
||||
if ('port-is-open' in response.arguments) {
|
||||
if ('port-is-open' in args) {
|
||||
setTextContent(element, is_open ? 'Open' : 'Closed');
|
||||
} else {
|
||||
setTextContent(element, 'Error');
|
||||
|
||||
@@ -8,6 +8,7 @@ export const RPC = {
|
||||
_DaemonVersion: 'version',
|
||||
_DownSpeedLimit: 'speed-limit-down',
|
||||
_DownSpeedLimited: 'speed-limit-down-enabled',
|
||||
_JsonRpcVersion: '2.0',
|
||||
_QueueMoveBottom: 'queue-move-bottom',
|
||||
_QueueMoveDown: 'queue-move-down',
|
||||
_QueueMoveTop: 'queue-move-top',
|
||||
@@ -46,12 +47,17 @@ export class Remote {
|
||||
})
|
||||
.then((response) => {
|
||||
response_argument = response;
|
||||
if (response.status === 409) {
|
||||
const error = new Error(Remote._SessionHeader);
|
||||
error.header = response.headers.get(Remote._SessionHeader);
|
||||
throw error;
|
||||
switch (response.status) {
|
||||
case 409: {
|
||||
const error = new Error(Remote._SessionHeader);
|
||||
error.header = response.headers.get(Remote._SessionHeader);
|
||||
throw error;
|
||||
}
|
||||
case 204:
|
||||
return null;
|
||||
default:
|
||||
return response.json();
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((payload) => {
|
||||
if (callback) {
|
||||
@@ -85,6 +91,8 @@ export class Remote {
|
||||
// TODO: return a Promise
|
||||
loadDaemonPrefs(callback, context) {
|
||||
const o = {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'session-get',
|
||||
};
|
||||
this.sendRequest(o, callback, context);
|
||||
@@ -92,36 +100,50 @@ export class Remote {
|
||||
|
||||
checkPort(ip_protocol, callback, context) {
|
||||
const o = {
|
||||
arguments: {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'port-test',
|
||||
params: {
|
||||
ip_protocol,
|
||||
},
|
||||
method: 'port-test',
|
||||
};
|
||||
this.sendRequest(o, callback, context);
|
||||
}
|
||||
|
||||
renameTorrent(torrentIds, oldpath, newname, callback, context) {
|
||||
const o = {
|
||||
arguments: {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-rename-path',
|
||||
params: {
|
||||
ids: torrentIds,
|
||||
name: newname,
|
||||
path: oldpath,
|
||||
},
|
||||
method: 'torrent-rename-path',
|
||||
};
|
||||
this.sendRequest(o, callback, context);
|
||||
}
|
||||
|
||||
setLabels(torrentIds, labels, callback) {
|
||||
const args = {
|
||||
const params = {
|
||||
ids: torrentIds,
|
||||
labels,
|
||||
};
|
||||
this.sendRequest({ arguments: args, method: 'torrent-set' }, callback);
|
||||
this.sendRequest(
|
||||
{
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-set',
|
||||
params,
|
||||
},
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
loadDaemonStats(callback, context) {
|
||||
const o = {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'session-stats',
|
||||
};
|
||||
this.sendRequest(o, callback, context);
|
||||
@@ -129,43 +151,46 @@ export class Remote {
|
||||
|
||||
updateTorrents(torrentIds, fields, callback, context) {
|
||||
const o = {
|
||||
arguments: {
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-get',
|
||||
params: {
|
||||
fields,
|
||||
format: 'table',
|
||||
},
|
||||
method: 'torrent-get',
|
||||
};
|
||||
if (torrentIds) {
|
||||
o.arguments.ids = torrentIds;
|
||||
o.params.ids = torrentIds;
|
||||
}
|
||||
this.sendRequest(o, (response) => {
|
||||
const arguments_ = response['arguments'];
|
||||
callback.call(context, arguments_.torrents, arguments_.removed);
|
||||
const { torrents, removed } = response.result;
|
||||
callback.call(context, torrents, removed);
|
||||
});
|
||||
}
|
||||
|
||||
getFreeSpace(dir, callback, context) {
|
||||
const o = {
|
||||
arguments: {
|
||||
path: dir,
|
||||
},
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'free-space',
|
||||
params: { path: dir },
|
||||
};
|
||||
this.sendRequest(o, (response) => {
|
||||
const arguments_ = response['arguments'];
|
||||
callback.call(context, arguments_.path, arguments_['size-bytes']);
|
||||
const { path, 'size-bytes': size_bytes } = response.result;
|
||||
callback.call(context, path, size_bytes);
|
||||
});
|
||||
}
|
||||
|
||||
changeFileCommand(torrentId, fileIndices, command) {
|
||||
const arguments_ = {
|
||||
const params = {
|
||||
ids: [torrentId],
|
||||
};
|
||||
arguments_[command] = fileIndices;
|
||||
params[command] = fileIndices;
|
||||
this.sendRequest(
|
||||
{
|
||||
arguments: arguments_,
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-set',
|
||||
params,
|
||||
},
|
||||
() => {
|
||||
this._controller.refreshTorrents([torrentId]);
|
||||
@@ -173,14 +198,14 @@ export class Remote {
|
||||
);
|
||||
}
|
||||
|
||||
sendTorrentSetRequests(method, torrent_ids, arguments_, callback, context) {
|
||||
if (!arguments_) {
|
||||
arguments_ = {};
|
||||
}
|
||||
arguments_['ids'] = torrent_ids;
|
||||
sendTorrentSetRequests(method, torrent_ids, params, callback, context) {
|
||||
params ||= {};
|
||||
params.ids = torrent_ids;
|
||||
const o = {
|
||||
arguments: arguments_,
|
||||
id: 'webui',
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
this.sendRequest(o, callback, context);
|
||||
}
|
||||
@@ -217,16 +242,17 @@ export class Remote {
|
||||
|
||||
removeTorrents(torrents, trash) {
|
||||
const o = {
|
||||
arguments: {
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-remove',
|
||||
params: {
|
||||
'delete-local-data': trash,
|
||||
ids: [],
|
||||
},
|
||||
method: 'torrent-remove',
|
||||
};
|
||||
|
||||
if (torrents) {
|
||||
for (let index = 0, length_ = torrents.length; index < length_; ++index) {
|
||||
o.arguments.ids.push(torrents[index].getId());
|
||||
o.params.ids.push(torrents[index].getId());
|
||||
}
|
||||
}
|
||||
this.sendRequest(o, () => {
|
||||
@@ -254,20 +280,22 @@ export class Remote {
|
||||
url = `magnet:?xt=urn:btih:${url}`;
|
||||
}
|
||||
const o = {
|
||||
arguments: {
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'torrent-add',
|
||||
params: {
|
||||
filename: url,
|
||||
paused: options.paused,
|
||||
},
|
||||
method: 'torrent-add',
|
||||
};
|
||||
this.sendRequest(o, () => {
|
||||
this._controller.refreshTorrents();
|
||||
});
|
||||
}
|
||||
savePrefs(arguments_) {
|
||||
savePrefs(params) {
|
||||
const o = {
|
||||
arguments: arguments_,
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'session-set',
|
||||
params,
|
||||
};
|
||||
this.sendRequest(o, () => {
|
||||
this._controller.loadDaemonPrefs();
|
||||
@@ -275,6 +303,7 @@ export class Remote {
|
||||
}
|
||||
updateBlocklist() {
|
||||
const o = {
|
||||
jsonrpc: RPC._JsonRpcVersion,
|
||||
method: 'blocklist-update',
|
||||
};
|
||||
this.sendRequest(o, () => {
|
||||
|
||||
@@ -65,8 +65,8 @@ export class RenameDialog extends EventTarget {
|
||||
file_path,
|
||||
new_name,
|
||||
(response) => {
|
||||
if (response.result === 'success') {
|
||||
const args = response.arguments;
|
||||
if ('result' in response) {
|
||||
const args = response.result;
|
||||
if (handler) {
|
||||
handler.subtree.name = args.name;
|
||||
setTextContent(handler.name_container, args.name);
|
||||
@@ -85,11 +85,13 @@ export class RenameDialog extends EventTarget {
|
||||
} else {
|
||||
tor.refresh(args);
|
||||
}
|
||||
} else if (response.result === 'Invalid argument') {
|
||||
} else {
|
||||
const error_obj = response.error;
|
||||
const err_msg =
|
||||
error_obj.data?.errorString ?? error_obj.message ?? '';
|
||||
const connection_alert = new AlertDialog({
|
||||
heading: `Error renaming "${file_path}"`,
|
||||
message:
|
||||
'Could not rename a torrent or file name. The path to file may have changed/not reflected correctly or the argument is invalid.',
|
||||
message: `${err_msg} (${error_obj.code}`,
|
||||
});
|
||||
this.controller.setCurrentPopup(connection_alert);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class StatisticsDialog extends EventTarget {
|
||||
this.remote = remote;
|
||||
|
||||
const updateDaemon = () =>
|
||||
this.remote.loadDaemonStats((data) => this._update(data.arguments));
|
||||
this.remote.loadDaemonStats((data) => this._update(data.result));
|
||||
const delay_msec = 5000;
|
||||
this.interval = setInterval(updateDaemon, delay_msec);
|
||||
updateDaemon();
|
||||
|
||||
@@ -353,7 +353,7 @@ export class Transmission extends EventTarget {
|
||||
|
||||
loadDaemonPrefs() {
|
||||
this.remote.loadDaemonPrefs((data) => {
|
||||
this.session_properties = data.arguments;
|
||||
this.session_properties = data.result;
|
||||
this._openTorrentFromUrl();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user