const std = @import("std"); const http = std.http; const util = @import("util.zig"); pub const Request = @import("request.zig"); pub const Response = @import("response.zig").Response; 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 torrentAdd(client: *Client, r: Request.TorrentAdd) !Response { const body = try torrentAddRaw(client, r); defer client.allocator.free(body); return Response.parseBody( client.allocator, Request.Method.@"torrent-add", body, null, ); } pub fn torrentAddRaw(client: *Client, r: Request.TorrentAdd) ![]u8 { const body = try client.do(Request.Object{ .method = .@"torrent-add", .arguments = .{ .@"torrent-add" = r }, }); return body; } pub fn torrentGet(client: *Client, r: ?Request.TorrentGet) !Response { const body = try torrentGetRaw(client, r); defer client.allocator.free(body); return Response.parseBody( client.allocator, Request.Method.@"torrent-get", body, null, ); } pub fn torrentGetRaw(client: *Client, r: ?Request.TorrentGet) ![]u8 { const request = blk: { if (r == null) { break :blk Request.Object{ .method = .@"torrent-get", .arguments = .{ .@"torrent-get" = Request.TorrentGet{ .fields = Request.TorrentGet.all_fields, }, }, }; } else { break :blk Request.Object{ .method = .@"torrent-get", .arguments = .{ .@"torrent-get" = r.? }, }; } }; const body = try client.do(request); return body; } //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_(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; //}