const std = @import("std"); const util = @import("util.zig"); pub const Request = @This(); pub const Method = enum { // Torrent requests: Torrent action // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#31-torrent-action-requests @"torrent-start", @"torrent-start-now", @"torrent-stop", @"torrent-verify", @"torrent-reannounce", // Torrent requests: Mutator // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#32-torrent-mutator-torrent-set @"torrent-set", // Torrent requests: Torrent accessor // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#33-torrent-accessor-torrent-get @"torrent-get", // Torrent requests: Torrent adding // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#34-adding-a-torrent @"torrent-add", // Torrent requests: Torrent removing // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#35-removing-a-torrent @"torrent-remove", // Torrent requests: Torrent moving // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#36-moving-a-torrent @"torrent-set-location", // Torrent requests: Torrent moving // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#37-renaming-a-torrents-path @"torrent-rename-path", // Session requests: Get // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#4--session-requests @"session-get", // Session requests: Mutator // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#411-mutators @"session-set", // TODO //@"session-stats", pub fn jsonStringify(self: Method, options: std.json.StringifyOptions, out_stream: anytype) !void { try std.json.stringify(@tagName(self), options, out_stream); } }; pub const TorrentIDs = union(enum) { single: i32, many: []const i32, @"recently-active", pub fn jsonStringify(self: TorrentIDs, options: std.json.StringifyOptions, out_stream: anytype) !void { switch (self) { .single => |v| try std.json.stringify(v, options, out_stream), .many => |v| try std.json.stringify(v, options, out_stream), .@"recently-active" => try std.json.stringify("recently-active", options, out_stream), } } }; // all equal just to we can switch on them at the same time pub const TorrentStart = struct { ids: TorrentIDs }; pub const TorrentStartNow = TorrentStart; pub const TorrentStop = TorrentStart; pub const TorrentVerify = TorrentStart; pub const TorrentReannounce = TorrentStart; pub const TorrentAdd = struct { // path to download the torrent to @"download-dir": ?[]const u8 = null, filename: ?[]const u8 = null, labels: ?[][]const u8 = null, // base64-encoded .torrent content metainfo: ?[]const u8 = null, // if true, don't start the torrent paused: ?bool = null, // TODO: // maximum number of peers //@"peer-limit": ?i32 = null, //cookies string pointer to a string of one or more cookies. //bandwidthPriority number torrent's bandwidth tr_priority_t //files-wanted array indices of file(s) to download //files-unwanted array indices of file(s) to not download //priority-high array indices of high-priority file(s) //priority-low array indices of low-priority file(s) //priority-normal array indices of normal-priority file(s) }; pub const TorrentGet = struct { pub const Fields = enum { activityDate, addedDate, availability, bandwidthPriority, comment, corruptEver, creator, dateCreated, desiredAvailable, doneDate, downloadDir, downloadedEver, downloadLimit, downloadLimited, editDate, @"error", errorString, eta, etaIdle, @"file-count", files, fileStats, group, hashString, haveUnchecked, haveValid, honorsSessionLimits, id, isFinished, isPrivate, isStalled, labels, leftUntilDone, magnetLink, manualAnnounceTime, maxConnectedPeers, metadataPercentComplete, name, @"peer-limit", peers, peersConnected, peersFrom, peersGettingFromUs, peersSendingToUs, percentComplete, percentDone, pieces, pieceCount, pieceSize, priorities, @"primary-mime-type", queuePosition, rateDownload, rateUpload, recheckProgress, secondsDownloading, secondsSeeding, seedIdleLimit, seedIdleMode, seedRatioLimit, seedRatioMode, sequentialDownload, sizeWhenDone, startDate, status, trackers, trackerList, trackerStats, totalSize, torrentFile, uploadedEver, uploadLimit, uploadLimited, uploadRatio, wanted, webseeds, webseedsSendingToUs, pub fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void { try std.json.stringify(@tagName(self), options, out_stream); } }; pub const all_fields = util.enumFieldsSlice(@This().Fields); ids: ?TorrentIDs = null, fields: []const Fields, }; pub const SessionGet = struct { pub const Fields = enum { @"alt-speed-down", @"alt-speed-enabled", @"alt-speed-time-begin", @"alt-speed-time-day", @"alt-speed-time-enabled", @"alt-speed-time-end", @"alt-speed-up", @"blocklist-enabled", @"blocklist-size", @"blocklist-url", @"cache-size-mb", @"config-dir", @"default-trackers", @"dht-enabled", @"download-dir", @"download-dir-free-space", @"download-queue-enabled", @"download-queue-size", encryption, @"idle-seeding-limit-enabled", @"idle-seeding-limit", @"incomplete-dir-enabled", @"incomplete-dir", @"lpd-enabled", @"peer-limit-global", @"peer-limit-per-torrent", @"peer-port-random-on-start", @"peer-port", @"pex-enabled", @"port-forwarding-enabled", @"queue-stalled-enabled", @"queue-stalled-minutes", @"rename-partial-files", @"rpc-version-minimum", @"rpc-version-semver", @"rpc-version", @"script-torrent-added-enabled", @"script-torrent-added-filename", @"script-torrent-done-enabled", @"script-torrent-done-filename", @"script-torrent-done-seeding-enabled", @"script-torrent-done-seeding-filename", @"seed-queue-enabled", @"seed-queue-size", seedRatioLimit, seedRatioLimited, @"speed-limit-down-enabled", @"speed-limit-down", @"speed-limit-up-enabled", @"speed-limit-up", @"start-added-torrents", @"trash-original-torrent-files", units, @"utp-enabled", version, pub fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void { try std.json.stringify(@tagName(self), options, out_stream); } }; pub const all_fields = util.enumFieldsSlice(@This().Fields); fields: []const Fields, }; pub const SessionSet = struct { @"alt-speed-down": ?usize = null, @"alt-speed-enabled": ?bool = null, @"alt-speed-time-begin": ?usize = null, @"alt-speed-time-day": ?usize = null, @"alt-speed-time-enabled": ?bool = null, @"alt-speed-time-end": ?usize = null, @"alt-speed-up": ?usize = null, @"blocklist-enabled": ?bool = null, @"blocklist-url": ?[]u8 = null, @"cache-size-mb": ?usize = null, @"default-trackers": ?[]u8 = null, @"dht-enabled": ?bool = null, @"download-dir": ?[]u8 = null, @"download-dir-free-space": ?usize = null, @"download-queue-enabled": ?bool = null, @"download-queue-size": ?usize = null, encryption: ?enum { required, preferred, tolerated, pub fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void { try std.json.stringify(@tagName(self), options, out_stream); } } = null, @"idle-seeding-limit-enabled": ?bool = null, @"idle-seeding-limit": ?usize = null, @"incomplete-dir-enabled": ?bool = null, @"incomplete-dir": ?[]u8 = null, @"lpd-enabled": ?bool = null, @"peer-limit-global": ?usize = null, @"peer-limit-per-torrent": ?usize = null, @"peer-port-random-on-start": ?bool = null, @"peer-port": ?usize = null, @"pex-enabled": ?bool = null, @"port-forwarding-enabled": ?bool = null, @"queue-stalled-enabled": ?bool = null, @"queue-stalled-minutes": ?usize = null, @"rename-partial-files": ?bool = null, @"script-torrent-added-enabled": ?bool = null, @"script-torrent-added-filename": ?[]u8 = null, @"script-torrent-done-enabled": ?bool = null, @"script-torrent-done-filename": ?[]u8 = null, @"script-torrent-done-seeding-enabled": ?bool = null, @"script-torrent-done-seeding-filename": ?[]u8 = null, @"seed-queue-enabled": ?bool = null, @"seed-queue-size": ?usize = null, seedRatioLimit: ?f64 = null, seedRatioLimited: ?bool = null, @"speed-limit-down-enabled": ?bool = null, @"speed-limit-down": ?usize = null, @"speed-limit-up-enabled": ?bool = null, @"speed-limit-up": ?usize = null, @"start-added-torrents": ?bool = null, @"trash-original-torrent-files": ?bool = null, @"utp-enabled": ?bool = null, }; pub const Object = struct { method: Method, arguments: union(Method) { @"torrent-start": TorrentStart, @"torrent-start-now": TorrentStartNow, @"torrent-stop": TorrentStop, @"torrent-verify": TorrentVerify, @"torrent-reannounce": TorrentReannounce, @"torrent-set": u8, @"torrent-get": TorrentGet, @"torrent-add": TorrentAdd, @"torrent-remove": u8, @"torrent-set-location": u8, @"torrent-rename-path": u8, @"session-get": SessionGet, @"session-set": SessionSet, const Self = @This(); pub fn jsonStringify(self: Self, _: std.json.StringifyOptions, out_stream: anytype) !void { const options = std.json.StringifyOptions{ .emit_null_optional_fields = false, }; switch (self) { .@"torrent-start", .@"torrent-start-now", .@"torrent-stop", .@"torrent-verify", .@"torrent-reannounce", => |v| try std.json.stringify(v, options, out_stream), .@"torrent-get" => |v| try std.json.stringify(v, options, out_stream), .@"torrent-add" => |v| try std.json.stringify(v, options, out_stream), .@"session-set" => |v| try std.json.stringify(v, options, out_stream), .@"session-get" => |v| try std.json.stringify(v, options, out_stream), else => unreachable, } } }, }; test "json request encoding" { const test_case = struct { name: []const u8, expected: []const u8, request: Object, fn run(self: @This()) !void { var req = std.ArrayList(u8).init(std.testing.allocator); defer req.deinit(); try std.json.stringify(self.request, .{}, req.writer()); std.testing.expect(std.mem.eql(u8, req.items, self.expected)) catch { std.debug.print("name: {s}\n", .{self.name}); std.debug.print("expected: \t{s}\n", .{self.expected}); std.debug.print("got: \t\t{s}\n", .{req.items}); return error.InvalidOutput; }; } }; const test_cases = [_]test_case{ .{ .name = "torrent-add", .request = .{ .method = .@"torrent-add", .arguments = .{ .@"torrent-add" = .{ .filename = "foobar", }, }, }, .expected = \\{"method":"torrent-add","arguments":{"filename":"foobar"}} , }, .{ .name = "session-get", .request = .{ .method = .@"session-get", .arguments = .{ .@"session-get" = .{ .fields = &[_]SessionGet.Fields{ .version, .@"utp-enabled", }, }, }, }, .expected = \\{"method":"session-get","arguments":{"fields":["version","utp-enabled"]}} , }, .{ .name = "session-set", .request = .{ .method = .@"session-set", .arguments = .{ .@"session-set" = .{ .@"lpd-enabled" = true, .encryption = .required, }, }, }, .expected = \\{"method":"session-set","arguments":{"encryption":"required","lpd-enabled":true}} , }, .{ .name = "torrent-reannounce single id", .request = .{ .method = .@"torrent-reannounce", .arguments = .{ .@"torrent-reannounce" = .{ .ids = .{ .single = 1 }, }, }, }, .expected = \\{"method":"torrent-reannounce","arguments":{"ids":1}} , }, .{ .name = "torrent-reannounce multiple id", .request = .{ .method = .@"torrent-reannounce", .arguments = .{ .@"torrent-reannounce" = .{ .ids = .{ .many = &[_]i32{ 1, 2 } }, }, }, }, .expected = \\{"method":"torrent-reannounce","arguments":{"ids":[1,2]}} , }, .{ .name = "torrent-reannounce recently-active", .request = .{ .method = .@"torrent-reannounce", .arguments = .{ .@"torrent-reannounce" = .{ .ids = .@"recently-active", }, }, }, .expected = \\{"method":"torrent-reannounce","arguments":{"ids":"recently-active"}} , }, .{ .name = "torrent-get", .request = .{ .method = .@"torrent-get", .arguments = .{ .@"torrent-get" = .{ .fields = &[_]TorrentGet.Fields{ .id, .name, }, .ids = .@"recently-active", }, }, }, .expected = \\{"method":"torrent-get","arguments":{"ids":"recently-active","fields":["id","name"]}} , }, }; for (test_cases) |tc| try tc.run(); }