const std = @import("std"); const util = @import("util.zig"); 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, //session_stats, const Self = @This(); pub const json_map = util.JsonMap(Self, util.replaceUnderscores); pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { try util.jsonStringify(self, options, out_stream); } }; pub const TorrentIDs = union(enum) { single: usize, many: []const usize, recently_active, const Self = @This(); pub fn jsonStringify(self: Self, 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 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, const Self = @This(); pub const json_map = util.JsonMap(Self, util.replaceUnderscores); pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { try util.jsonStringify(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, const Self = @This(); pub const json_map = util.JsonMap(Self, util.replaceUnderscores); pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { try util.jsonStringify(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, const Self = @This(); pub const json_map = util.JsonMap(Self, util.replaceUnderscores); pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { try util.jsonStringify(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 json_map = util.JsonMap(SessionSet, util.replaceUnderscores); pub fn jsonStringify(self: SessionSet, options: std.json.StringifyOptions, out_stream: anytype) !void { try util.jsonStringify(self, options, out_stream); } }; pub const Request = 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: u8, 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), .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: Request, 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 = "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 = &[_]usize{ 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(); }