const std = @import("std"); const http = std.http; const util = @import("util.zig"); const Request = @import("request.zig"); const Response = @import("response.zig"); pub const ClientOptions = extern struct { host: [*:0]const u8, port: u16, https: bool, user: ?[*:0]const u8 = null, password: ?[*:0]const u8 = null, }; pub const Client = struct { uri: std.Uri, allocator: std.mem.Allocator, http_client: http.Client, http_headers: http.Headers, current_session_id: []u8 = "", pub fn init(allocator: std.mem.Allocator, opts: ClientOptions) Client { const base_url = std.Uri{ .path = "/transmission/rpc", .scheme = blk: { if (opts.https) { break :blk "https"; } else { break :blk "http"; } }, .host = std.mem.span(opts.host), .port = opts.port, .user = null, .password = null, .query = null, .fragment = null, }; return Client{ .uri = base_url, .allocator = allocator, .http_client = http.Client{ .allocator = allocator }, .http_headers = http.Headers{ .allocator = allocator }, }; } pub fn deinit(self: *Client) void { self.http_headers.deinit(); self.http_client.deinit(); self.allocator.free(self.current_session_id); } fn setSessionId(self: *Client, r: http.Client.Response) !void { self.allocator.free(self.current_session_id); const session_id = r.headers.getFirstValue("X-Transmission-Session-Id") orelse return error.NoSessionId; self.current_session_id = try std.fmt.allocPrint(self.allocator, "{s}", .{session_id}); } fn newRequest(self: *Client) !http.Client.Request { try self.http_headers.append("X-Transmission-Session-Id", self.current_session_id); return try self.http_client.request(.POST, self.uri, self.http_headers, .{}); } pub fn do(self: *Client, req: Request.Object) ![]u8 { var real_req = try self.newRequest(); defer real_req.deinit(); var payload = std.ArrayList(u8).init(self.allocator); defer payload.deinit(); try std.json.stringify(req, .{}, payload.writer()); real_req.transfer_encoding = http.Client.RequestTransfer{ .content_length = payload.items.len, }; try real_req.start(); if (try real_req.write(payload.items) != payload.items.len) { return error.InvalidSize; } try real_req.finish(); try real_req.wait(); if (real_req.response.status == http.Status.conflict) { try self.setSessionId(real_req.response); return self.do(req); } var body = std.ArrayList(u8).init(self.allocator); // TODO: making max_append_size this large // all the time doesn't feel right try real_req.reader().readAllArrayList(&body, 9000000000); return body.toOwnedSlice(); } }; pub fn session_get_raw(client: *Client, session_get: ?Request.SessionGet) ![]u8 { const default: Request.SessionGet = .{ .fields = Request.SessionGet.all_fields, }; const r = Request.Object{ .method = .session_get, .arguments = .{ .session_get = session_get orelse default }, }; const body = try client.do(r); return body; } pub fn torrent_get_raw(client: *Client, torrent_get: ?Request.TorrentGet) ![]u8 { const default: Request.TorrentGet = .{ .fields = Request.TorrentGet.all_fields, }; //const default: Request.TorrentGet = .{ //.fields = &[_]Request.TorrentGet.Fields{.name}, //}; const r = Request.Object{ .method = .torrent_get, .arguments = .{ .torrent_get = torrent_get orelse default }, }; const body = try client.do(r); return body; } pub fn torrent_get_(client: *Client, torrent_get: ?Request.TorrentGet) !Response.Object { @setEvalBranchQuota(100000); const body = try torrent_get_raw(client, torrent_get); const decoded: Response.TorrentGet = try std.json.parseFromSlice( Response.TorrentGet, client.allocator, body, std.json.ParseOptions{ .ignore_unknown_fields = true, .duplicate_field_behavior = .@"error", }, ); return Response.Object.init(.{ .torrent_get = decoded.arguments }, decoded.result); } pub fn session_set_raw(client: *Client, session_set: Request.SessionSet) ![]u8 { const r = Request.Object{ .method = .session_set, .arguments = .{ .session_set = session_set }, }; const body = try client.do(r); return body; }