Changes
52 changed files (+2148/-2490)
-
-
@@ -23,7 +23,6 @@const std = @import("std"); const CompileVala = @import("./build/CompileVala.zig"); const LibRoon = @import("./build/LibRoon.zig"); const BuildError = error{ NonValaSourceInSourceListError,
-
@@ -58,18 +57,6 @@break :libsood lib; }; const libroon = LibRoon.init(b, .{ .target = target, .optimize = optimize, }); // `zig build emit-libroon` { const step = b.step("emit-libroon", "Emit libroon library files."); step.dependOn(libroon.step); step.dependOn(libroon.install_pretty_headers); } // System libraries to link. const system_libraries = [_][]const u8{ "gtk4",
-
@@ -79,6 +66,7 @@ // that purpose, whilst undocumented."librsvg-2.0", "gee-0.8", "libsoup-3.0", "json-glib-1.0", }; const vala_main = vala_main: {
-
@@ -103,15 +91,15 @@ // that purpose, whilst undocumented."librsvg-2.0", "gee-0.8", "libsoup-3.0", "json-glib-1.0", }); compile.addPackage("posix"); compile.addPackages(&.{ "libsood", "roon" }); compile.addPackages(&.{"libsood"}); compile.addGResourceXML(b.path("data/gresource.xml")); compile.addVapi(b.path("src/libsood.vapi")); compile.addVapi(libroon.lib.getEmittedIncludeTree().path(b, "roon.vapi")); const step = b.step("valac", "Compile C source code from Vala files for debugging purpose"); step.dependOn(compile.step);
-
@@ -172,7 +160,6 @@ exe.linkSystemLibrary(lib);} exe.root_module.linkLibrary(libsood); exe.root_module.linkLibrary(libroon.lib); exe.addCSourceFile(.{ .file = gresouce_c });
-
-
build/LibRoon.zig (deleted)
-
@@ -1,113 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const RoonMsg = @This(); const std = @import("std"); step: *std.Build.Step, lib: *std.Build.Step.Compile, /// Install formatted header and vapi files. /// For debugging. install_pretty_headers: *std.Build.Step, pub const Options = struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, }; pub fn init(b: *std.Build, opts: Options) RoonMsg { const lib = b.addLibrary(.{ .name = "libroon", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/lib.zig"), .link_libc = true, .target = opts.target, .optimize = opts.optimize, }), }); const spec_builder = b.addExecutable(.{ .name = "build_api_spec", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/build_api_spec.zig"), .target = b.graph.host, .link_libc = true, }), }); const build_spec = b.addRunArtifact(spec_builder); const spec_out = build_spec.addOutputFileArg("spec.json"); const header_builder = b.addExecutable(.{ .name = "build_header", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/build_c_header.zig"), .target = b.graph.host, }), }); const build_header = b.addRunArtifact(header_builder); build_header.addFileArg(spec_out); const header_out = build_header.addOutputFileArg("roon.h"); const vapi_builder = b.addExecutable(.{ .name = "build_vapi", .root_module = b.createModule(.{ .root_source_file = b.path("src/Roon/build_vapi.zig"), .target = b.graph.host, }), }); const build_vapi = b.addRunArtifact(vapi_builder); build_vapi.addFileArg(spec_out); const vapi_out = build_vapi.addOutputFileArg("roon.vapi"); lib.installHeader(header_out, "roon.h"); lib.installHeader(vapi_out, "roon.vapi"); const uncrustify_vala = b.addSystemCommand(&.{"uncrustify"}); uncrustify_vala.addArg("-c"); uncrustify_vala.addFileArg(b.path("uncrustify.cfg")); uncrustify_vala.addArgs(&.{ "-l", "VALA" }); uncrustify_vala.setStdIn(.{ .lazy_path = vapi_out }); // Uncrustify outputs to stderr anyways _ = uncrustify_vala.captureStdErr(); const pretty_vapi = uncrustify_vala.captureStdOut(); const uncrustify_c = b.addSystemCommand(&.{"uncrustify"}); uncrustify_c.addArg("-c"); uncrustify_c.addFileArg(b.path("uncrustify.cfg")); uncrustify_c.addArgs(&.{ "-l", "C" }); uncrustify_c.setStdIn(.{ .lazy_path = header_out }); // Uncrustify outputs to stderr anyways _ = uncrustify_c.captureStdErr(); const pretty_header = uncrustify_c.captureStdOut(); const install_pretty_header = b.addInstallHeaderFile(pretty_header, "roon.pretty.h"); const install_pretty_vapi = b.addInstallHeaderFile(pretty_vapi, "roon.pretty.vapi"); install_pretty_vapi.step.dependOn(&install_pretty_header.step); const install = b.addInstallArtifact(lib, .{}); return .{ .step = &install.step, .lib = lib, .install_pretty_headers = &install_pretty_vapi.step, }; }
-
-
-
@@ -26,6 +26,7 @@ libadwaita,librsvg, libsoup_3, libgee, json-glib, vala, coreutils, wrapGAppsHook4,
-
@@ -103,6 +104,10 @@ # > HTTP client/server library for GNOME# https://gitlab.gnome.org/GNOME/libsoup # https://valadoc.org/libsoup-3.0/index.htm libsoup_3 # > Library providing (de)serialization support for the JavaScript Object Notation (JSON) format # https://gitlab.gnome.org/GNOME/json-glib json-glib # This helper takes care of GLib/GTK's messy runtime things. # https://nixos.org/manual/nixpkgs/stable/#sec-language-gnome
-
-
-
@@ -186,10 +186,6 @@ } catch (GLib.Error error) {throw new ConnectError.INVALID_RESPONSE(error.message); } if (info == null) { throw new ConnectError.INVALID_RESPONSE("Unable to parse info response: Out of memory."); } return info; }
-
@@ -212,13 +208,7 @@ }GLib.log("Plac", LEVEL_DEBUG, "Confirmed the Roon server is running. Registering extension."); var req_json = req.to_json(); if (req_json == null) { throw new ConnectError.INVALID_REQUEST("Unable to serialize register request."); } var register_resp = yield send_request(conn, "com.roonlabs.registry:1/register", 2, req_json); var register_resp = yield send_request(conn, "com.roonlabs.registry:1/register", 2, req.to_json()); if (register_resp.meta.service != "Registered") { throw new ConnectError.INVALID_RESPONSE(@"$(register_resp.meta.service)"); }
-
@@ -230,10 +220,6 @@result = new Roon.Registry.Register.Response.from_json(body.data); } catch (GLib.Error error) { throw new ConnectError.INVALID_RESPONSE(error.message); } if (result == null) { throw new ConnectError.INVALID_RESPONSE("Unable to parse info response: Out of memory."); } GLib.log("Plac", LEVEL_DEBUG, @"Registered Roon extension at $(this.address.address):$(this.http_port)");
-
-
-
@@ -126,16 +126,12 @@ if (meta.service != "Changed" || headers.request_id != res.headers.request_id) {return; } Moo.JsonBody event_body; Roon.Transport.SubscribeZoneChanges.Event event; try { event_body = new Moo.JsonBody.from_string(message, headers); } catch (Moo.JsonBodyParseError error) { GLib.log("Plac", LEVEL_WARNING, "Got zone change event, but payload is unparsable."); return; } var event = new Roon.Transport.SubscribeZoneChanges.Event.from_json(event_body.data); if (event == null) { GLib.log("Plac", LEVEL_WARNING, "Got unexpected zone change event."); var event_body = new Moo.JsonBody.from_string(message, headers); event = new Roon.Transport.SubscribeZoneChanges.Event.from_json(event_body.data); } catch (GLib.Error error) { GLib.log("Plac", LEVEL_WARNING, "Got unparsable zone change event: %s", error.message); return; }
-
@@ -191,10 +187,10 @@ for (uint i = 0; i < zones.n_items; i++) {var model = (ZoneModel) zones.get_item(i); if (model.zone.zone_id == seek_change.zone_id && model.zone.now_playing != null) { if (seek_change.has_seek_position) { model.zone.now_playing.has_seek_info = true; model.zone.now_playing.has_seek_position = true; model.zone.now_playing.seek_position = seek_change.seek_position; } else { model.zone.now_playing.has_seek_info = false; model.zone.now_playing.has_seek_position = false; } this.seek(seek_change); break;
-
-
-
src/Roon/ApiSpec.zig (deleted)
-
@@ -1,136 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); items: []const Item, pub const CBaseType = union(enum) { int: u16, uint: u16, float: void, double: void, char: void, bool: void, void: void, string: void, custom: []const u8, pub fn fromZigType(comptime T: type) CBaseType { return switch (@typeInfo(T)) { .int => |int| if (int.signedness == .signed) .{ .int = int.bits, } else .{ .uint = int.bits, }, .float => |float| switch (float.bits) { 32 => .float, 64 => .double, else => @compileError(std.fmt.comptimePrint("{d} bits float is not exportable.", .{float.bits})), }, .bool => .bool, .@"enum" => .{ .custom = T.cname }, .@"struct" => .{ .custom = T.cname }, .void => .void, .pointer => |pointer| switch (@typeInfo(pointer.child)) { .@"struct" => .{ .custom = pointer.child.cname }, else => { if (pointer.child == u8 and pointer.is_const and pointer.size == .many and std.builtin.Type.Pointer.sentinel(pointer) == 0) { return .string; } @compileError( std.fmt.comptimePrint("Pointer to non-struct type {s} is not exportable.", .{@typeName(T)}), ); }, }, else => { @compileError( std.fmt.comptimePrint("Type {s} is not exportable.", .{@typeName(T)}), ); }, }; } }; pub const CType = union(enum) { value: CBaseType, pointer: struct { type: CBaseType, @"const": bool, }, pub fn fromZigType(comptime T: type) CType { return switch (@typeInfo(T)) { .pointer => |pointer| { if (pointer.child == u8 and pointer.is_const and pointer.size == .many and std.builtin.Type.Pointer.sentinel(pointer) == 0) { return .{ .value = .string, }; } return .{ .pointer = .{ .type = CBaseType.fromZigType(pointer.child), .@"const" = pointer.is_const, } }; }, .optional => |optional| fromZigType(optional.child), else => .{ .value = CBaseType.fromZigType(T) }, }; } }; pub const EnumField = struct { name: []const u8, value: c_int, }; pub const FunctionParameter = struct { type: CType, }; pub const Function = struct { name: []const u8, parameters: []const FunctionParameter, return_type: CType, }; pub const StructField = struct { name: []const u8, type: CType, }; pub const Item = union(enum) { @"enum": struct { cname: []const u8, fields: []const EnumField, }, @"struct": struct { cname: []const u8, new_function: ?Function = null, member_functions: []const Function, fields: []const StructField, }, @"opaque": struct { cname: []const u8, new_function: Function, member_functions: []const Function, }, };
-
-
-
@@ -0,0 +1,84 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Browse { namespace Browse { public class Request : Object { public Hierarchy hierarchy { get; set construct; } public string? item_key { get; set; default = null; } public string? input { get; set; default = null; } public string? zone_or_output_id { get; set; default = null; } public bool pop_all { get; set; default = false; } public size_t pop_levels { get; set; default = 0; } public bool refresh_list { get; set; default = false; } public Request(Hierarchy hierarchy) { Object(hierarchy: hierarchy); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("hierarchy"); this.hierarchy.json_add(builder); if (this.item_key != null) { builder.set_member_name("item_key"); builder.add_string_value(this.item_key); } if (this.input != null) { builder.set_member_name("input"); builder.add_string_value(this.input); } if (this.zone_or_output_id != null) { builder.set_member_name("zone_or_output_id"); builder.add_string_value(this.zone_or_output_id); } if (this.pop_all) { builder.set_member_name("pop_all"); builder.add_boolean_value(true); } if (this.pop_levels > 0) { builder.set_member_name("pop_levels"); builder.add_int_value(this.pop_levels); } if (this.refresh_list) { builder.set_member_name("refresh_list"); builder.add_boolean_value(true); } builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,84 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { namespace Browse { public class Response : Object { public BrowseAction action { get; construct; } public Item? item { get; construct; } public List? list { get; construct; } public string? message { get; construct; } public bool is_error { get; construct; } internal Response.from_json_node(Json.Node node) throws Error { var path = new FieldPath("Roon.Browse.Browse.Response"); var obj = get_object(node, path); BrowseAction action; Item? item = null; List? list = null; string? message = null; bool is_error = false; var action_path = new FieldPath("action", path); action = BrowseAction.from_json_node(get_member(obj, action_path), action_path); var item_path = new FieldPath("item", path); var item_node = get_optional_member(obj, item_path); if (item_node != null) { item = new Item.from_json_node(item_node, item_path); } var list_path = new FieldPath("list", path); var list_node = get_optional_member(obj, list_path); if (list_node != null) { list = new List.from_json_node(list_node, list_path); } var message_path = new FieldPath("message", path); var message_node = get_optional_member(obj, message_path); if (message_node != null) { message = get_string(message_node, message_path); } var is_error_path = new FieldPath("is_error", path); var is_error_node = get_optional_member(obj, is_error_path); if (is_error_node != null) { is_error = get_boolean(is_error_node, is_error_path); } Object( action: action, item: item, list: list, message: message, is_error: is_error ); } public Response.from_json(string json) throws Error { var parser = new Json.Parser(); parser.load_from_data(json); this.from_json_node(parser.get_root()); } } } } }
-
-
-
@@ -0,0 +1,48 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { public enum BrowseAction { MESSAGE = 0, NONE = 1, LIST = 2, REPLACE_ITEM = 3, REMOVE_ITEM = 4; internal static BrowseAction from_json_node(Json.Node node, FieldPath? path = null) throws Error { var str = (!)get_string(node, path); switch (str) { case "message": return MESSAGE; case "none": return NONE; case "list": return LIST; case "replace_item": return REPLACE_ITEM; case "remove_item": return REMOVE_ITEM; default: throw new ParseError.INVALID_BROWSE_ACTION(@"Unknown browse action \"$(str)\""); } } } } }
-
-
-
@@ -0,0 +1,61 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Browse { public enum Hierarchy { BROWSE = 0, PLAYLISTS = 1, SETTINGS = 2, INTERNET_RADIO = 3, ALBUMS = 4, ARTISTS = 5, GENRES = 6, COMPOSERS = 7, SEARCH = 8; public string to_id_string() { switch (this) { case BROWSE: return "browse"; case PLAYLISTS: return "playlists"; case SETTINGS: return "settings"; case INTERNET_RADIO: return "internet_radio"; case ALBUMS: return "albums"; case ARTISTS: return "artists"; case GENRES: return "genres"; case COMPOSERS: return "composers"; case SEARCH: // Vala does not support closed enum / exhaustive switch. default: return "search"; } } public void json_add(Json.Builder builder) { builder.add_string_value(this.to_id_string()); } } } }
-
-
-
@@ -0,0 +1,84 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { public class Item : Object { public string title { get; construct; } public string? subtitle { get; construct; } public string? image_key { get; construct; } public string? item_key { get; construct; } public ItemHint hint { get; construct; } public ItemInputPrompt? input_prompt { get; construct; } internal Item.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string title; string? subtitle = null; string? image_key = null; string? item_key = null; ItemHint hint = UNKNOWN; ItemInputPrompt? input_prompt = null; var title_path = new FieldPath("title", obj_path); title = get_string(get_member(obj, title_path), title_path); var subtitle_path = new FieldPath("subtitle", obj_path); var subtitle_node = get_optional_member(obj, subtitle_path); if (subtitle_node != null) { subtitle = get_string(subtitle_node, subtitle_path); } var image_key_path = new FieldPath("image_key", obj_path); var image_key_node = get_optional_member(obj, image_key_path); if (image_key_node != null) { image_key = get_string(image_key_node, image_key_path); } var item_key_path = new FieldPath("item_key", obj_path); var item_key_node = get_optional_member(obj, item_key_path); if (item_key_node != null) { item_key = get_string(item_key_node, item_key_path); } var hint_path = new FieldPath("hint", obj_path); var hint_node = get_optional_member(obj, hint_path); if (hint_node != null) { hint = ItemHint.from_json_node(hint_node, hint_path); } var input_prompt_path = new FieldPath("input_prompt", obj_path); var input_prompt_node = get_optional_member(obj, input_prompt_path); if (input_prompt_node != null) { input_prompt = new ItemInputPrompt.from_json_node(input_prompt_node, input_prompt_path); } Object( title: title, subtitle: subtitle, image_key: image_key, item_key: item_key, hint: hint, input_prompt: input_prompt ); } } } }
-
-
-
@@ -0,0 +1,46 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { public enum ItemHint { UNKNOWN = 0, ACTION = 1, ACTION_LIST = 2, LIST = 3, HEADER = 4; internal static ItemHint from_json_node(Json.Node node, FieldPath? path = null) throws Error { switch (get_string(node, path)) { case "action": return ACTION; case "action_list": return ACTION_LIST; case "list": return LIST; case "header": return HEADER; default: return UNKNOWN; } } } } }
-
-
-
@@ -0,0 +1,59 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { public class ItemInputPrompt : Object { public string prompt { get; construct; } public string action { get; construct; } public string? value { get; construct; } public bool is_password { get; construct; } internal ItemInputPrompt.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string prompt; string action; string? value = null; bool is_password = false; var prompt_path = new FieldPath("prompt", obj_path); prompt = get_string(get_member(obj, prompt_path), prompt_path); var action_path = new FieldPath("action", obj_path); action = get_string(get_member(obj, action_path), action_path); var value_path = new FieldPath("value", obj_path); var value_node = get_optional_member(obj, value_path); if (value_node != null) { value = get_string(value_node, value_path); } var is_password_path = new FieldPath("is_password", obj_path); var is_password_node = get_optional_member(obj, is_password_path); if (is_password_node != null) { is_password = get_boolean(is_password_node, is_password_path); } Object(prompt: prompt, action: action, value: value, is_password: is_password); } } } }
-
-
-
@@ -0,0 +1,98 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { public class List : Object { public string title { get; construct; } public size_t count { get; construct; } public string? subtitle { get; construct; } public string? image_key { get; construct; } public size_t level { get; construct; } public bool has_display_offset { get; construct; } public int64 display_offset { get; construct; } public ListHint hint { get; construct; } internal List.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string title; size_t count = 0; string? subtitle = null; string? image_key = null; size_t level = 0; bool has_display_offset = false; int64 display_offset = 0; ListHint hint = UNKNOWN; var title_path = new FieldPath("title", obj_path); title = get_string(get_member(obj, title_path), title_path); var count_path = new FieldPath("count", obj_path); var count_node = get_optional_member(obj, count_path); if (count_node != null) { count = (size_t) get_int(count_node, count_path); } var subtitle_path = new FieldPath("subtitle", obj_path); var subtitle_node = get_optional_member(obj, subtitle_path); if (subtitle_node != null) { subtitle = get_string(subtitle_node, subtitle_path); } var image_key_path = new FieldPath("image_key", obj_path); var image_key_node = get_optional_member(obj, image_key_path); if (image_key_node != null) { image_key = get_string(image_key_node, image_key_path); } var level_path = new FieldPath("level", obj_path); var level_node = get_optional_member(obj, level_path); if (level_node != null) { level = (size_t) get_int(level_node, level_path); } var display_offset_path = new FieldPath("display_offset", obj_path); var display_offset_node = get_optional_member(obj, display_offset_path); if (display_offset_node != null) { display_offset = get_int(display_offset_node, display_offset_path); has_display_offset = true; } var hint_path = new FieldPath("hint", obj_path); var hint_node = get_optional_member(obj, hint_path); if (hint_node != null) { hint = ListHint.from_json_node(hint_node, hint_path); } Object( title: title, count: count, subtitle: subtitle, image_key: image_key, level: level, has_display_offset: has_display_offset, display_offset: display_offset, hint: hint ); } } } }
-
-
-
@@ -0,0 +1,36 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { public enum ListHint { UNKNOWN = 0, ACTION_LIST = 1; internal static ListHint from_json_node(Json.Node node, FieldPath? path = null) throws Error { switch (get_string(node, path)) { case "action_list": return ACTION_LIST; default: return UNKNOWN; } } } } }
-
-
-
@@ -0,0 +1,93 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Browse { namespace Load { public class Request : Object { public Hierarchy hierarchy { get; set construct; } public bool has_set_display_offset { get; set; default = false; } public int64 set_display_offset { get; set; default = 0; } public bool has_level { get; set; default = false; } public size_t level { get; set; default = 0; } public bool has_offset { get; set; default = false; } public int64 offset { get; set; default = 0; } public size_t count { get; set; default = 0; } public string? multi_session_key { get; set; default = null; } public Request(Hierarchy hierarchy) { Object(hierarchy: hierarchy); } construct { this.notify["set-display-offset"].connect(() => { has_set_display_offset = true; }); this.notify["level"].connect(() => { has_level = true; }); this.notify["offset"].connect(() => { has_offset = true; }); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("hierarchy"); hierarchy.json_add(builder); if (has_set_display_offset) { builder.set_member_name("set_display_offset"); builder.add_int_value(set_display_offset); } if (has_level) { builder.set_member_name("level"); builder.add_int_value(level); } if (has_offset) { builder.set_member_name("offset"); builder.add_int_value(offset); } if (count > 0) { builder.set_member_name("count"); builder.add_int_value(count); } if (multi_session_key != null) { builder.set_member_name("multi_session_key"); builder.add_string_value(multi_session_key); } builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,66 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Browse { namespace Load { public class Response : Object { public Array<Item> items { get; construct; } public int64 offset { get; construct; } public List list { get; construct; } internal Response.from_json_node(Json.Node node) throws Error { var path = new FieldPath("Roon.Browse.Load.Response"); var obj = get_object(node, path); var items = new Array<Item>(); int64 offset = 0; List list; var items_path = new FieldPath("items", path); var items_arr = get_array(get_member(obj, items_path), items_path); for (uint i = 0, l = items_arr.get_length(); i < l; i++) { var item_path = new FieldPath(@"$i", items_path); var item_node = items_arr.get_element(i); var item = new Item.from_json_node(item_node, item_path); items.append_val(item); } var offset_path = new FieldPath("offset", path); var offset_node = get_optional_member(obj, offset_path); if (offset_node != null) { offset = get_int(offset_node, offset_path); } var list_path = new FieldPath("list", path); list = new List.from_json_node(get_member(obj, list_path), list_path); Object(items: items, offset: offset, list: list); } public Response.from_json(string json) throws Error { var parser = new Json.Parser(); parser.load_from_data(json); this.from_json_node(parser.get_root()); } } } } }
-
-
src/Roon/JsonHelper.vala (new)
-
@@ -0,0 +1,131 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; namespace Roon { /** * json-glib has unbelievably loose value getters. * This namespace provides sane getters wrapping original ones. * Setters in this class throws an error instead of incorrectly cast values. */ namespace JsonHelper { public errordomain ParseError { UNEXPECTED_TYPE, } internal class FieldPath { public FieldPath? parent; public string path; public FieldPath(string path, FieldPath? parent = null) { this.path = path; this.parent = parent; } public string to_string() { return (parent != null) ? @"$(parent).$(path)" : path; } public string to_prefix() { if (this == null) { return ""; } return @"$(this): "; } } internal static Json.Node get_member(Json.Object obj, FieldPath? path) throws ParseError { var node = obj.get_member(path.path); if (node == null) { throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected \"$(path.path)\" field, but the field is missing or null."); } return node; } internal static Json.Node? get_optional_member(Json.Object obj, FieldPath? path) { var node = obj.get_member(path.path); if (node == null || node.is_null()) { return null; } return node; } internal static string get_string(Json.Node node, FieldPath? path) throws ParseError { var str = node.get_string(); if (str == null) { throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected String, got $(node.get_value_type().to_string())."); } return str; } internal static bool get_boolean(Json.Node node, FieldPath? path) throws ParseError { var type = node.get_value_type(); if (type != Type.BOOLEAN) { throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected Boolean, got $(type.to_string())."); } return node.get_boolean(); } internal static int64 get_int(Json.Node node, FieldPath? path) throws ParseError { var type = node.get_value_type(); if (type != Type.INT64) { throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected Int, got $(type.to_string())"); } return node.get_int(); } internal static double get_double(Json.Node node, FieldPath? path) throws ParseError { var type = node.get_value_type(); switch (type) { case Type.DOUBLE: return node.get_double(); // JavaScript does not distinct integer and float. We can't assume float field does not // contain integer. case Type.INT64: return (double) node.get_int(); default: throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected Double, got $(type.to_string())"); } } internal static Json.Object get_object(Json.Node node, FieldPath? path) throws ParseError { var type = node.get_node_type(); if (type != Json.NodeType.OBJECT) { throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected Object, got $(type.to_string())"); } return (!) node.get_object(); } internal static Json.Array get_array(Json.Node node, FieldPath? path) throws ParseError { var type = node.get_node_type(); if (type != Json.NodeType.ARRAY) { throw new ParseError.UNEXPECTED_TYPE(@"$(path.to_prefix())Expected Array, got $(type.to_string())"); } return (!) node.get_array(); } } }
-
-
-
@@ -0,0 +1,56 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Registry { namespace Info { public class Response : Object { public string core_id { get; construct; } public string display_name { get; construct; } public string display_version { get; construct; } internal Response.from_json_node(Json.Node node) throws Error { var path = new FieldPath("Roon.Registry.Info.Response"); var obj = get_object(node, path); string core_id; string display_name; string display_version; var core_id_path = new FieldPath("core_id", path); core_id = get_string(get_member(obj, core_id_path), core_id_path); var display_name_path = new FieldPath("display_name", path); display_name = get_string(get_member(obj, display_name_path), display_name_path); var display_version_path = new FieldPath("display_version", path); display_version = get_string(get_member(obj, display_version_path), display_version_path); Object(core_id: core_id, display_name: display_name, display_version: display_version); } public Response.from_json(string json) throws Error { var parser = new Json.Parser(); parser.load_from_data(json); this.from_json_node(parser.get_root()); } } } } }
-
-
-
@@ -0,0 +1,105 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; namespace Roon { namespace Registry { namespace Register { public class Request : Object { public string? extension_id { get; set; } public string? display_name { get; set; } public string? display_version { get; set; } public string? publisher { get; set; } public string? email { get; set; } public string? token { get; set; } public Array<string> required_services = new Array<string>(); public Array<string> optional_services = new Array<string>(); public Array<string> provided_services = new Array<string>(); public Request() { Object(); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); if (extension_id != null) { builder.set_member_name("extension_id"); builder.add_string_value(extension_id); } if (display_name != null) { builder.set_member_name("display_name"); builder.add_string_value(display_name); } if (display_version != null) { builder.set_member_name("display_version"); builder.add_string_value(display_version); } if (publisher != null) { builder.set_member_name("publisher"); builder.add_string_value(publisher); } if (email != null) { builder.set_member_name("email"); builder.add_string_value(email); } if (token != null) { builder.set_member_name("token"); builder.add_string_value(token); } builder.set_member_name("required_services"); builder.begin_array(); for (int i = 0; i < required_services.length; i++) { builder.add_string_value(required_services.index(i)); } builder.end_array(); builder.set_member_name("optional_services"); builder.begin_array(); for (int i = 0; i < optional_services.length; i++) { builder.add_string_value(optional_services.index(i)); } builder.end_array(); builder.set_member_name("provided_services"); builder.begin_array(); for (int i = 0; i < provided_services.length; i++) { builder.add_string_value(provided_services.index(i)); } builder.end_array(); builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,51 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Registry { namespace Register { public class Response : Object { public string core_id { get; construct; } public string token { get; construct; } internal Response.from_json_node(Json.Node node) throws Error { var path = new FieldPath("Roon.Registry.Register.Response"); var obj = get_object(node, path); string core_id; string token; var core_id_path = new FieldPath("core_id", path); core_id = get_string(get_member(obj, core_id_path), core_id_path); var token_path = new FieldPath("token", path); token = get_string(get_member(obj, token_path), token_path); Object(core_id: core_id, token: token); } public Response.from_json(string json) throws Error { var parser = new Json.Parser(); parser.load_from_data(json); this.from_json_node(parser.get_root()); } } } } }
-
-
-
@@ -0,0 +1,54 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Transport { namespace ChangeVolume { public class Request : Object { public string output_id { get; set construct; } public ChangeVolumeMode how { get; set; default = RELATIVE; } public double value { get; set; default = 0.0; } public Request(string output_id) { Object(output_id: output_id); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("output_id"); builder.add_string_value(output_id); builder.set_member_name("how"); how.json_add(builder); builder.set_member_name("value"); builder.add_double_value(value); builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,45 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public enum ChangeVolumeMode { ABSOLUTE = 0, RELATIVE = 1, RELATIVE_STEP = 2; public string to_id_string() { switch (this) { case RELATIVE: return "relative"; case RELATIVE_STEP: return "relative_step"; case ABSOLUTE: // Vala does not support closed enum / exhaustive switch. default: return "absolute"; } } public void json_add(Json.Builder builder) { builder.add_string_value(this.to_id_string()); } } } }
-
-
-
@@ -0,0 +1,50 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Transport { namespace Control { public class Request : Object { public string zone_or_output_id { get; set construct; } public ControlType control { get; set construct; } public Request(string zone_or_output_id, ControlType control) { Object(zone_or_output_id: zone_or_output_id, control: control); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("zone_or_output_id"); builder.add_string_value(zone_or_output_id); builder.set_member_name("control"); control.json_add(builder); builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,54 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public enum ControlType { PLAY = 0, PAUSE = 1, PLAYPAUSE = 2, STOP = 3, PREVIOUS = 4, NEXT = 5; public string to_id_string() { switch (this) { case PAUSE: return "pause"; case PLAYPAUSE: return "playpause"; case STOP: return "stop"; case PREVIOUS: return "previous"; case NEXT: return "next"; case PLAY: // Vala does not support closed enum / exhaustive switch. default: return "play"; } } public void json_add(Json.Builder builder) { builder.add_string_value(this.to_id_string()); } } } }
-
-
-
@@ -0,0 +1,124 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public class NowPlaying : Object { public bool has_seek_position { get; set construct; } public uint64 seek_position { get; set construct; } public uint64 length { get; construct; } public string? image_key { get; construct; } public string one_line_1 { get; construct; } public string two_line_1 { get; construct; } public string? two_line_2 { get; construct; } public string three_line_1 { get; construct; } public string? three_line_2 { get; construct; } public string? three_line_3 { get; construct; } internal NowPlaying.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); bool has_seek_position = false; uint64 seek_position = 0; uint64 length = 0; string? image_key = null; string one_line_1; string two_line_1; string? two_line_2 = null; string three_line_1; string? three_line_2 = null; string? three_line_3 = null; var seek_position_path = new FieldPath("seek_position", obj_path); var seek_position_node = get_optional_member(obj, seek_position_path); if (seek_position_node != null) { seek_position = get_int(seek_position_node, seek_position_path); has_seek_position = true; } var length_path = new FieldPath("length", obj_path); var length_node = get_optional_member(obj, length_path); if (length_node != null) { length = get_int(length_node, length_path); } var image_key_path = new FieldPath("image_key", obj_path); var image_key_node = get_optional_member(obj, image_key_path); if (image_key_node != null) { image_key = get_string(image_key_node, image_key_path); } var one_line_path = new FieldPath("one_line", obj_path); var one_line = get_object(get_member(obj, one_line_path), one_line_path); var one_line_1_path = new FieldPath("line1", one_line_path); one_line_1 = get_string(get_member(one_line, one_line_1_path), one_line_1_path); var two_line_path = new FieldPath("two_line", obj_path); var two_line = get_object(get_member(obj, two_line_path), two_line_path); var two_line_1_path = new FieldPath("line1", two_line_path); two_line_1 = get_string(get_member(two_line, two_line_1_path), two_line_1_path); var two_line_2_path = new FieldPath("line2", two_line_path); var two_line_2_node = get_member(two_line, two_line_2_path); if (two_line_2_node != null) { two_line_2 = get_string(two_line_2_node, two_line_2_path); } var three_line_path = new FieldPath("three_line", obj_path); var three_line = get_object(get_member(obj, three_line_path), three_line_path); var three_line_1_path = new FieldPath("line1", three_line_path); three_line_1 = get_string(get_member(three_line, three_line_1_path), three_line_1_path); var three_line_2_path = new FieldPath("line2", three_line_path); var three_line_2_node = get_member(three_line, three_line_2_path); if (three_line_2_node != null) { three_line_2 = get_string(three_line_2_node, three_line_2_path); } var three_line_3_path = new FieldPath("line3", three_line_path); var three_line_3_node = get_member(three_line, three_line_3_path); if (three_line_3_node != null) { three_line_3 = get_string(three_line_3_node, three_line_3_path); } Object( has_seek_position: has_seek_position, seek_position: seek_position, length: length, image_key: image_key, one_line_1: one_line_1, two_line_1: two_line_1, two_line_2: two_line_2, three_line_1: three_line_1, three_line_2: three_line_2, three_line_3: three_line_3 ); } construct { this.notify["seek-position"].connect(() => { has_seek_position = true; }); } } } }
-
-
-
@@ -0,0 +1,54 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public class Output : Object { public string output_id { get; construct; } public string display_name { get; construct; } public OutputVolume? volume { get; construct; } internal Output.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string output_id; string display_name; OutputVolume? volume = null; var output_id_path = new FieldPath("output_id", obj_path); output_id = get_string(get_member(obj, output_id_path), output_id_path); var display_name_path = new FieldPath("display_name", obj_path); display_name = get_string(get_member(obj, display_name_path), display_name_path); var volume_path = new FieldPath("volume", obj_path); var volume_node = get_optional_member(obj, volume_path); if (volume_node != null) { volume = new OutputVolume.from_json_node(volume_node, volume_path); } Object( output_id: output_id, display_name: display_name, volume: volume ); } } } }
-
-
-
@@ -0,0 +1,100 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public class OutputVolume : Object { public string volume_type { get; construct; } public bool has_min { get; construct; } public double min { get; construct; } public bool has_max { get; construct; } public double max { get; construct; } public bool has_value { get; construct; } public double value { get; construct; } public bool has_step { get; construct; } public double step { get; construct; } public bool is_muted { get; construct; } internal OutputVolume.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string type; bool has_min = false; double min = 0.0; bool has_max = false; double max = 0.0; bool has_value = false; double value = 0.0; bool has_step = false; double step = 0.0; bool is_muted = false; var type_path = new FieldPath("type", obj_path); type = get_string(get_member(obj, type_path), type_path); var min_path = new FieldPath("min", obj_path); var min_node = get_optional_member(obj, min_path); if (min_node != null) { min = get_double(min_node, min_path); has_min = true; } var max_path = new FieldPath("max", obj_path); var max_node = get_optional_member(obj, max_path); if (max_node != null) { max = get_double(max_node, max_path); has_max = true; } var value_path = new FieldPath("value", obj_path); var value_node = get_optional_member(obj, value_path); if (value_node != null) { value = get_double(value_node, value_path); has_value = true; } var step_path = new FieldPath("step", obj_path); var step_node = get_optional_member(obj, step_path); if (step_node != null) { step = get_double(step_node, step_path); has_step = true; } var is_muted_path = new FieldPath("is_muted", obj_path); var is_muted_node = get_optional_member(obj, is_muted_path); if (is_muted_node != null) { is_muted = get_boolean(is_muted_node, is_muted_path); } Object( volume_type: type, has_min: has_min, min: min, has_max: has_max, max: max, has_value: has_value, value: value, has_step: has_step, step: step, is_muted: is_muted ); } } } }
-
-
-
@@ -0,0 +1,24 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Transport { public errordomain ParseError { INVALID_PLAYBACK_STATE, } } }
-
-
-
@@ -0,0 +1,45 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public enum PlaybackState { STOPPED = 0, PLAYING = 1, PAUSED = 2, LOADING = 3; internal static PlaybackState from_json_node(Json.Node node, FieldPath? path = null) throws Error { var str = (!)get_string(node, path); switch (str) { case "stopped": return STOPPED; case "playing": return PLAYING; case "paused": return PAUSED; case "loading": return LOADING; default: throw new ParseError.INVALID_PLAYBACK_STATE(@"Unknown playback state \"$(str)\""); } } } } }
-
-
-
@@ -0,0 +1,54 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Transport { namespace Seek { public class Request : Object { public string zone_or_output_id { get; set construct; } public SeekMode how { get; set; default = RELATIVE; } public int64 seconds { get; set; default = 0; } public Request(string zone_or_output_id) { Object(zone_or_output_id: zone_or_output_id); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("zone_or_output_id"); builder.add_string_value(zone_or_output_id); builder.set_member_name("how"); how.json_add(builder); builder.set_member_name("seconds"); builder.add_int_value(seconds); builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,52 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public class SeekChange : Object { public string zone_id { get; construct; } public bool has_seek_position { get; construct; } public uint64 seek_position { get; construct; } internal SeekChange.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string zone_id; bool has_seek_position = false; uint64 seek_position = 0; var zone_id_path = new FieldPath("zone_id", obj_path); zone_id = get_string(get_member(obj, zone_id_path), zone_id_path); var seek_position_path = new FieldPath("seek_position", obj_path); var seek_position_node = get_optional_member(obj, seek_position_path); if (seek_position_node != null) { seek_position = get_int(seek_position_node, seek_position_path); has_seek_position = true; } Object( zone_id: zone_id, has_seek_position: has_seek_position, seek_position: seek_position ); } } } }
-
-
-
@@ -0,0 +1,43 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public enum SeekMode { ABSOLUTE = 0, RELATIVE = 1; public string to_id_string() { switch (this) { case RELATIVE: return "relative"; case ABSOLUTE: // Vala does not support closed enum / exhaustive switch. default: return "absolute"; } } public void json_add(Json.Builder builder) { builder.add_string_value(this.to_id_string()); } } } }
-
-
-
@@ -0,0 +1,101 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { namespace SubscribeZoneChanges { public class Event : Object { public Array<string> removed_zone_ids { get; construct; } public Array<Zone> added_zones { get; construct; } public Array<Zone> changed_zones { get; construct; } public Array<SeekChange> seek_changes { get; construct; } internal Event.from_json_node(Json.Node node) throws Error { var path = new FieldPath("Roon.Transport.SubscribeZoneChanges.Event"); var obj = get_object(node, path); var removed_zone_ids = new Array<string>(); var added_zones = new Array<Zone>(); var changed_zones = new Array<Zone>(); var seek_changes = new Array<SeekChange>(); var removed_zone_ids_path = new FieldPath("zones_removed", path); var removed_zone_ids_node = get_optional_member(obj, removed_zone_ids_path); if (removed_zone_ids_node != null) { var removed_zone_ids_arr = get_array(removed_zone_ids_node, removed_zone_ids_path); for (uint i = 0, l = removed_zone_ids_arr.get_length(); i < l; i++) { var id_path = new FieldPath(@"$i", removed_zone_ids_path); var id = get_string(removed_zone_ids_arr.get_element(i), id_path); removed_zone_ids.append_val(id); } } var added_zones_path = new FieldPath("zones_added", path); var added_zones_node = get_optional_member(obj, added_zones_path); if (added_zones_node != null) { var added_zones_arr = get_array(added_zones_node, added_zones_path); for (uint i = 0, l = added_zones_arr.get_length(); i < l; i++) { var zone_path = new FieldPath(@"$i", added_zones_path); var zone_node = added_zones_arr.get_element(i); var zone = new Zone.from_json_node(zone_node, zone_path); added_zones.append_val(zone); } } var changed_zones_path = new FieldPath("zones_changed", path); var changed_zones_node = get_optional_member(obj, changed_zones_path); if (changed_zones_node != null) { var changed_zones_arr = get_array(changed_zones_node, changed_zones_path); for (uint i = 0, l = changed_zones_arr.get_length(); i < l; i++) { var zone_path = new FieldPath(@"$i", changed_zones_path); var zone_node = changed_zones_arr.get_element(i); var zone = new Zone.from_json_node(zone_node, zone_path); changed_zones.append_val(zone); } } var seek_changes_path = new FieldPath("zones_seek_changed", path); var seek_changes_node = get_optional_member(obj, seek_changes_path); if (seek_changes_node != null) { var seek_changes_arr = get_array(seek_changes_node, seek_changes_path); for (uint i = 0, l = seek_changes_arr.get_length(); i < l; i++) { var change_path = new FieldPath(@"$i", seek_changes_path); var change_node = seek_changes_arr.get_element(i); var change = new SeekChange.from_json_node(change_node, change_path); seek_changes.append_val(change); } } Object( removed_zone_ids: removed_zone_ids, added_zones: added_zones, changed_zones: changed_zones, seek_changes: seek_changes ); } public Event.from_json(string json) throws Error { var parser = new Json.Parser(); parser.load_from_data(json); this.from_json_node(parser.get_root()); } } } } }
-
-
-
@@ -0,0 +1,46 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 namespace Roon { namespace Transport { namespace SubscribeZoneChanges { public class Request : Object { public string subscription_key { get; set construct; } public Request(string subscription_key) { Object(subscription_key: subscription_key); } public string to_json() { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("subscription_key"); builder.add_string_value(subscription_key); builder.end_object(); var generator = new Json.Generator(); var root = builder.get_root(); generator.set_root(root); return generator.to_data(null); } } } } }
-
-
-
@@ -0,0 +1,52 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { namespace SubscribeZoneChanges { public class Response : Object { public Array<Zone> zones { get; construct; } internal Response.from_json_node(Json.Node node) throws Error { var path = new FieldPath("Roon.Transport.SubscribeZoneChanges.Response"); var obj = get_object(node, path); var zones = new Array<Zone>(); var zones_path = new FieldPath("zones", path); var zones_arr = get_array(get_member(obj, zones_path), zones_path); for (uint i = 0, l = zones_arr.get_length(); i < l; i++) { var zone_path = new FieldPath(@"$i", zones_path); var zone_node = zones_arr.get_element(i); var zone = new Zone.from_json_node(zone_node, zone_path); zones.append_val(zone); } Object(zones: zones); } public Response.from_json(string json) throws Error { var parser = new Json.Parser(); parser.load_from_data(json); this.from_json_node(parser.get_root()); } } } } }
-
-
-
@@ -0,0 +1,117 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 using GLib; using Roon.JsonHelper; namespace Roon { namespace Transport { public class Zone : Object { public string zone_id { get; construct; } public string display_name { get; construct; } public Array<Output> outputs { get; construct; } public NowPlaying? now_playing { get; construct; } public PlaybackState state { get; construct; } public bool is_next_allowed { get; construct; } public bool is_previous_allowed { get; construct; } public bool is_pause_allowed { get; construct; } public bool is_play_allowed { get; construct; } public bool is_seek_allowed { get; construct; } internal Zone.from_json_node(Json.Node node, FieldPath? obj_path = null) throws Error { var obj = get_object(node, obj_path); string zone_id; string display_name; var outputs = new Array<Output>(); NowPlaying? now_playing = null; PlaybackState state; bool is_next_allowed = false; bool is_previous_allowed = false; bool is_pause_allowed = false; bool is_play_allowed = false; bool is_seek_allowed = false; var zone_id_path = new FieldPath("zone_id", obj_path); zone_id = get_string(get_member(obj, zone_id_path), zone_id_path); var display_name_path = new FieldPath("display_name", obj_path); display_name = get_string(get_member(obj, display_name_path), display_name_path); var outputs_path = new FieldPath("outputs", obj_path); var outputs_arr = get_array(get_member(obj, outputs_path), outputs_path); for (uint i = 0, l = outputs_arr.get_length(); i < l; i++) { var output_path = new FieldPath(@"$i", outputs_path); var output_node = outputs_arr.get_element(i); var output = new Output.from_json_node(output_node, output_path); outputs.append_val(output); } var now_playing_path = new FieldPath("now_playing", obj_path); var now_playing_node = get_optional_member(obj, now_playing_path); if (now_playing_node != null) { now_playing = new NowPlaying.from_json_node(now_playing_node, now_playing_path); } var state_path = new FieldPath("state", obj_path); state = PlaybackState.from_json_node(get_member(obj, state_path), state_path); var is_next_allowed_path = new FieldPath("is_next_allowed", obj_path); var is_next_allowed_node = get_optional_member(obj, is_next_allowed_path); if (is_next_allowed_node != null) { is_next_allowed = get_boolean(is_next_allowed_node, is_next_allowed_path); } var is_previous_allowed_path = new FieldPath("is_previous_allowed", obj_path); var is_previous_allowed_node = get_optional_member(obj, is_previous_allowed_path); if (is_previous_allowed_node != null) { is_previous_allowed = get_boolean(is_previous_allowed_node, is_previous_allowed_path); } var is_pause_allowed_path = new FieldPath("is_pause_allowed", obj_path); var is_pause_allowed_node = get_optional_member(obj, is_pause_allowed_path); if (is_pause_allowed_node != null) { is_pause_allowed = get_boolean(is_pause_allowed_node, is_pause_allowed_path); } var is_play_allowed_path = new FieldPath("is_play_allowed", obj_path); var is_play_allowed_node = get_optional_member(obj, is_play_allowed_path); if (is_play_allowed_node != null) { is_play_allowed = get_boolean(is_play_allowed_node, is_play_allowed_path); } var is_seek_allowed_path = new FieldPath("is_seek_allowed", obj_path); var is_seek_allowed_node = get_optional_member(obj, is_seek_allowed_path); if (is_seek_allowed_node != null) { is_seek_allowed = get_boolean(is_seek_allowed_node, is_seek_allowed_path); } Object( zone_id: zone_id, display_name: display_name, outputs: outputs, now_playing: now_playing, state: state, is_next_allowed: is_next_allowed, is_previous_allowed: is_previous_allowed, is_pause_allowed: is_pause_allowed, is_play_allowed: is_play_allowed, is_seek_allowed: is_seek_allowed ); } } } }
-
-
src/Roon/api.zig (deleted)
-
@@ -1,51 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const browse = @import("./services/browse.zig"); const registry = @import("./services/registry.zig"); const transport = @import("./services/transport.zig"); pub const public_api: []const type = &.{ browse.Hierarchy, browse.ItemHint, browse.ItemInputPrompt, browse.Item, browse.ListHint, browse.List, browse.BrowseAction, browse.browse.Request, browse.browse.Response, browse.load.Request, browse.load.Response, registry.info.Response, registry.register.Request, registry.register.Response, transport.NowPlaying, transport.PlaybackState, transport.OutputVolume, transport.Output, transport.Zone, transport.SeekChange, transport.ControlType, transport.SeekMode, transport.ChangeVolumeMode, transport.SubscribeZoneChanges.Request, transport.SubscribeZoneChanges.Response, transport.SubscribeZoneChanges.Event, transport.Control.Request, transport.Seek.Request, transport.ChangeVolume.Request, };
-
-
src/Roon/build_api_spec.zig (deleted)
-
@@ -1,191 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const ApiSpec = @import("./ApiSpec.zig"); const public_api = @import("./api.zig").public_api; fn CSetterValue(comptime T: type) type { return switch (@typeInfo(T)) { .pointer => |pointer| switch (pointer.size) { .slice => if (pointer.is_const) [*:0]const pointer.child else [*:0]pointer.child, else => @compileError("Non-slice pointer is not supported."), }, .optional => |optional| CSetterValue(optional.child), else => T, }; } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var args = std.process.ArgIterator.init(); defer args.deinit(); _ = args.skip(); const output = args.next() orelse { return error.NotEnoughArgument; }; if (args.next()) |_| { return error.TooManyArgument; } const items = try allocator.alloc(ApiSpec.Item, public_api.len); errdefer allocator.free(items); inline for (public_api, 0..) |Api, i| { switch (@typeInfo(Api)) { .@"enum" => |info| { const fields = try allocator.alloc(ApiSpec.EnumField, info.fields.len); inline for (info.fields, 0..) |field, j| { fields[j] = .{ .name = try std.ascii.allocUpperString(allocator, field.name), .value = field.value, }; } items[i] = .{ .@"enum" = .{ .cname = Api.cname, .fields = fields, } }; }, .@"struct" => |_| { const self_pointer: ApiSpec.CType = .{ .pointer = .{ .@"const" = false, .type = .{ .custom = Api.Data.cname }, }, }; const ref: ApiSpec.Function = .{ .name = "retain", .parameters = &.{.{ .type = self_pointer }}, .return_type = self_pointer, }; const unref: ApiSpec.Function = .{ .name = "release", .parameters = &.{.{ .type = self_pointer }}, .return_type = .{ .value = .void }, }; const info = @typeInfo(Api.Data).@"struct"; if (info.layout == .@"extern") { // Plain struct or Deserializable const fields = try allocator.alloc(ApiSpec.StructField, info.fields.len); inline for (info.fields, 0..) |field, j| { fields[j] = .{ .name = field.name, .type = .fromZigType(field.type), }; } const from_json: ApiSpec.Function = .{ .name = "from_json", .parameters = &.{ .{ .type = .{ .pointer = .{ .@"const" = true, .type = .char, }, } }, }, .return_type = self_pointer, }; items[i] = .{ .@"struct" = .{ .cname = Api.Data.cname, .fields = fields, .new_function = if (@hasDecl(Api.Data, "deserializable") and Api.Data.deserializable) from_json else null, .member_functions = &.{ ref, unref }, }, }; continue; } // Serializable const init = @typeInfo(@TypeOf(Api.Data.init)).@"fn"; const init_params = try allocator.alloc(ApiSpec.FunctionParameter, init.params.len); inline for (init.params, 0..) |param, j| { init_params[j] = .{ .type = if (param.type.? == []const u8) .{ .value = .string } else .fromZigType(param.type.?), }; } const methods = try allocator.alloc(ApiSpec.Function, info.fields.len + 3); methods[info.fields.len + 0] = ref; methods[info.fields.len + 1] = unref; methods[info.fields.len + 2] = .{ .name = "to_json", .parameters = &.{.{ .type = self_pointer }}, .return_type = .fromZigType([*:0]const u8), }; inline for (info.fields, 0..) |field, j| { if (field.type == [][]const u8) { methods[j] = .{ .name = std.fmt.comptimePrint("append_{s}", .{field.name}), .parameters = &.{ .{ .type = self_pointer }, .{ .type = .fromZigType([*:0]const u8) }, }, .return_type = .{ .value = .void }, }; } else { methods[j] = .{ .name = std.fmt.comptimePrint("set_{s}", .{field.name}), .parameters = &.{ .{ .type = self_pointer }, .{ .type = .fromZigType(CSetterValue(field.type)) }, }, .return_type = .{ .value = .void }, }; } } items[i] = .{ .@"opaque" = .{ .cname = Api.Data.cname, .new_function = .{ .name = "new", .parameters = init_params, .return_type = self_pointer, }, .member_functions = methods, }, }; }, else => { @compileError(std.fmt.comptimePrint("Cannot export {s}", .{@typeName(Api)})); }, } } var output_file = try std.fs.cwd().createFile(output, .{}); defer output_file.close(); const writer = output_file.writer(); try std.json.stringify(ApiSpec{ .items = items }, .{}, writer); return std.process.cleanExit(); }
-
-
src/Roon/build_c_header.zig (deleted)
-
@@ -1,171 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const ApiSpec = @import("./ApiSpec.zig"); fn writeBaseType(writer: anytype, t: ApiSpec.CBaseType) !void { switch (t) { .bool, .char, .float, .double, .void => try writer.writeAll(@tagName(t)), .custom => |name| try writer.writeAll(name), .string => try writer.writeAll("const char *"), .int, .uint => |bits| { try std.fmt.format(writer, "{s}{d}_t", .{ if (t == .int) "int" else "uint", bits, }); }, } } fn writeType(writer: anytype, t: ApiSpec.CType) !void { switch (t) { .pointer => |pointer| { if (pointer.@"const") { try writer.writeAll("const "); } try writeBaseType(writer, pointer.type); try writer.writeAll(" *"); }, .value => |child| try writeBaseType(writer, child), } } fn writeFunction(writer: anytype, cname: []const u8, f: ApiSpec.Function) !void { try writeType(writer, f.return_type); try std.fmt.format(writer, " {s}_{s}(", .{ cname, f.name }); for (f.parameters, 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } try writeType(writer, param.type); } try writer.writeAll(");\n"); } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var args = std.process.ArgIterator.init(); defer args.deinit(); _ = args.skip(); const input = args.next() orelse { return error.NoInputFile; }; const output = args.next() orelse { return error.NoOutputFile; }; if (args.next()) |_| { return error.TooManyArgument; } const input_file = try std.fs.cwd().openFile(input, .{}); defer input_file.close(); var source = std.json.reader(allocator, input_file.reader()); const parsed = try std.json.parseFromTokenSource(ApiSpec, allocator, &source, .{}); defer parsed.deinit(); const spec = parsed.value; const output_file = try std.fs.cwd().createFile(output, .{}); defer output_file.close(); const writer = output_file.writer(); try writer.writeAll( \\#ifndef ROON_MSG_H \\#define ROON_MSG_H \\ \\#include <stdbool.h> \\#include <stddef.h> \\#include <stdint.h> \\ \\ ); for (spec.items) |item| { switch (item) { .@"enum" => |info| { try writer.writeAll("typedef enum {\n"); const ns = try std.ascii.allocUpperString(allocator, info.cname); for (info.fields) |field| { try std.fmt.format(writer, "\t{s}_{s} = {d},\n", .{ ns, try std.ascii.allocUpperString(allocator, field.name), field.value, }); } try std.fmt.format(writer, "}} {s};\n\n", .{info.cname}); }, .@"opaque" => |info| { try std.fmt.format(writer, "typedef void {s};\n", .{info.cname}); try writeFunction(writer, info.cname, info.new_function); for (info.member_functions) |member_fn| { try writeFunction(writer, info.cname, member_fn); } try writer.writeByte('\n'); }, .@"struct" => |info| { try writer.writeAll("typedef struct {\n"); // Zig has compiler bug that prevents using tab inside multiline string. try writer.writeAll("\tsize_t __rc;\n"); for (info.fields) |field| { try writer.writeByte('\t'); try writeType(writer, field.type); if (field.type == .pointer and std.mem.endsWith(u8, field.name, "_ptr") and field.type.pointer.type == .custom) { try writer.writeByte('*'); } try writer.writeByte(' '); try std.fmt.format(writer, "{s};\n", .{field.name}); } try std.fmt.format(writer, "}} {s};\n", .{info.cname}); if (info.new_function) |f| { try writeFunction(writer, info.cname, f); } for (info.member_functions) |f| { try writeFunction(writer, info.cname, f); } try writer.writeByte('\n'); }, } } try writer.writeAll("#endif\n"); }
-
-
src/Roon/build_vapi.zig (deleted)
-
@@ -1,294 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const ApiSpec = @import("./ApiSpec.zig"); fn writeBaseType(writer: anytype, t: ApiSpec.CBaseType) !void { switch (t) { .bool, .char, .float, .double, .string, .void => try writer.writeAll(@tagName(t)), .custom => |name| try writeCNameAsValaName(writer, name), .int => |bits| try std.fmt.format(writer, "int{d}", .{bits}), .uint => |bits| try std.fmt.format(writer, "uint{d}", .{bits}), } } fn writeType(writer: anytype, t: ApiSpec.CType) !void { switch (t) { .pointer => |pointer| { if (pointer.@"const" and pointer.type == .char) { try writer.writeAll("string"); return; } try writeBaseType(writer, pointer.type); }, .value => |child| try writeBaseType(writer, child), } } fn cnameToNamespaces(allocator: std.mem.Allocator, cname: []const u8) ![]const []const u8 { var ns = std.ArrayList([]const u8).init(allocator); defer ns.deinit(); var iter = std.mem.splitScalar(u8, cname, '_'); while (iter.next()) |token| { const buf = try allocator.dupe(u8, token); if (buf.len == 0) { continue; } buf[0] = std.ascii.toUpper(buf[0]); try ns.append(buf); } return ns.toOwnedSlice(); } fn writeCNameAsValaName(writer: anytype, cname: []const u8) !void { var iter = std.mem.splitScalar(u8, cname, '_'); while (iter.next()) |token| { if (token.len == 0) { continue; } try writer.writeByte(std.ascii.toUpper(token[0])); try writer.writeAll(token[1..]); if (iter.peek()) |_| { try writer.writeByte('.'); } } } const CompactClassOptions = struct { name: []const u8, cname: []const u8, new_function: ?ApiSpec.Function = null, member_functions: []const ApiSpec.Function = &.{}, fields: []const ApiSpec.StructField = &.{}, }; fn writeCompactClass(writer: anytype, opts: CompactClassOptions) !void { try writer.writeAll("[CCode (\n"); try std.fmt.format(writer, "\tcname = \"{s}\",\n", .{opts.cname}); try std.fmt.format(writer, "\tref_function = \"{s}_retain\",\n", .{opts.cname}); try std.fmt.format(writer, "\tunref_function = \"{s}_release\"\n", .{opts.cname}); try writer.writeAll(")]\n[Compact]\n"); try std.fmt.format(writer, "public class {s} {{\n", .{opts.name}); if (opts.new_function) |f| { try std.fmt.format(writer, "[CCode (cname = \"{s}_{s}\")]\n", .{ opts.cname, f.name }); try std.fmt.format(writer, "public {s}", .{opts.name}); if (!std.mem.eql(u8, f.name, "new")) { try std.fmt.format(writer, ".{s}", .{f.name}); } try writer.writeByte('('); for (f.parameters, 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } try writeType(writer, param.type); try std.fmt.format(writer, " arg{d}", .{i}); } try writer.writeAll(");\n"); } for (opts.fields) |field| { if (std.mem.endsWith(u8, field.name, "_len")) { continue; } if (std.mem.endsWith(u8, field.name, "_ptr") and field.type == .pointer) { try std.fmt.format( writer, "[CCode (\n\tcname = \"{s}\",\n\tarray_length_cname = \"{s}_len\", array_length_type = \"size_t\"\n)]\n", .{ field.name, field.name[0 .. field.name.len - 4] }, ); try writer.writeAll("\tpublic "); try writeType(writer, field.type); try std.fmt.format(writer, "[] {s};\n", .{ field.name[0 .. field.name.len - 4], }); continue; } try writer.writeAll("\tpublic "); try writeType(writer, field.type); try std.fmt.format(writer, " {s};\n", .{field.name}); } for (opts.member_functions) |f| { if (std.mem.startsWith(u8, f.name, "set_") and f.return_type == .value and f.return_type.value == .void) { if (f.parameters.len != 2) { std.log.err("Setter {s} on {s} does not have 2 parameters.", .{ f.name, opts.cname }); return error.SetterNotHavingTwoParameters; } const value = f.parameters[1]; try writer.writeAll("public "); try writeType(writer, value.type); try std.fmt.format(writer, " {s} {{\n\t[CCode (cname = \"{s}_{s}\")] set;\n}}\n", .{ f.name[4..], opts.cname, f.name, }); continue; } try std.fmt.format(writer, "[CCode (cname = \"{s}_{s}\")]\n", .{ opts.cname, f.name, }); try writer.writeAll("public "); try writeType(writer, f.return_type); try std.fmt.format(writer, " {s}(", .{f.name}); for (f.parameters[1..], 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } try writeType(writer, param.type); try std.fmt.format(writer, " arg{d}", .{i}); } try writer.writeAll(");\n"); } try writer.writeAll("}\n"); } pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var args = std.process.ArgIterator.init(); defer args.deinit(); _ = args.skip(); const input = args.next() orelse { return error.NoInputFile; }; const output = args.next() orelse { return error.NoOutputFile; }; if (args.next()) |_| { return error.TooManyArgument; } const input_file = try std.fs.cwd().openFile(input, .{}); defer input_file.close(); var source = std.json.reader(allocator, input_file.reader()); const parsed = try std.json.parseFromTokenSource(ApiSpec, allocator, &source, .{}); defer parsed.deinit(); const spec = parsed.value; const output_file = try std.fs.cwd().createFile(output, .{}); defer output_file.close(); const writer = output_file.writer(); for (spec.items) |item| { switch (item) { .@"enum" => |info| { const components = try cnameToNamespaces(allocator, info.cname); const ns = components[0 .. components.len - 1]; const name = components[components.len - 1]; try writer.writeAll("[CCode (cheader_filename = \"roon.h\")]\n"); for (ns) |token| { try std.fmt.format(writer, "namespace {s} {{\n", .{token}); } try writer.writeAll("[CCode (\n"); try std.fmt.format(writer, "\tcname = \"{s}\",\n", .{info.cname}); try std.fmt.format(writer, "\tcprefix = \"{s}_\",\n", .{ try std.ascii.allocUpperString(allocator, info.cname), }); try writer.writeAll("\thas_type_id = false\n)]\n"); try std.fmt.format(writer, "public enum {s} {{\n", .{name}); for (info.fields) |field| { try std.fmt.format(writer, "\t{s} = {d},\n", .{ try std.ascii.allocUpperString(allocator, field.name), field.value, }); } try writer.writeAll("}\n"); for (ns) |_| { try writer.writeAll("}\n"); } }, .@"struct" => |info| { const components = try cnameToNamespaces(allocator, info.cname); const ns = components[0 .. components.len - 1]; const name = components[components.len - 1]; try writer.writeAll("[CCode (cheader_filename = \"roon.h\")]\n"); for (ns) |token| { try std.fmt.format(writer, "namespace {s} {{\n", .{token}); } try writeCompactClass(writer, .{ .name = name, .cname = info.cname, .fields = info.fields, .new_function = info.new_function, .member_functions = info.member_functions, }); for (ns) |_| { try writer.writeAll("}\n"); } }, .@"opaque" => |info| { const components = try cnameToNamespaces(allocator, info.cname); const ns = components[0 .. components.len - 1]; const name = components[components.len - 1]; try writer.writeAll("[CCode (cheader_filename = \"roon.h\")]\n"); for (ns) |token| { try std.fmt.format(writer, "namespace {s} {{\n", .{token}); } try writeCompactClass(writer, .{ .name = name, .cname = info.cname, .new_function = info.new_function, .member_functions = info.member_functions, }); for (ns) |_| { try writer.writeAll("}\n"); } }, } try writer.writeByte('\n'); } }
-
-
src/Roon/json.zig (deleted)
-
@@ -1,192 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); pub const SerializableOptions = struct { field: []const u8 = "data", json_options: std.json.StringifyOptions = .{ .emit_null_optional_fields = false, }, }; pub fn Serializable(comptime T: type, opts: SerializableOptions) type { const to_json = struct { fn to_json(ptr: *const T) callconv(.C) ?[*:0]const u8 { var buf = std.ArrayList(u8).init(std.heap.c_allocator); defer buf.deinit(); const writer = buf.writer(); std.json.stringify( @field(ptr, opts.field), opts.json_options, writer, ) catch return null; const slice = buf.toOwnedSliceSentinel(0) catch return null; return slice.ptr; } }; comptime { if (@typeInfo(T).@"struct".layout == .@"extern") { @extern(&T, .{}); } @export(&to_json.to_json, .{ .name = std.fmt.comptimePrint("{s}_to_json", .{T.cname}), }); const Container = t: { for (@typeInfo(T).@"struct".fields) |field| { if (std.mem.eql(u8, field.name, opts.field)) { break :t field.type; } } @compileError(std.fmt.comptimePrint("Type \"{s}\" does not have \"{s}\" field", .{ @typeName(T), opts.field, })); }; for (@typeInfo(Container).@"struct".fields) |field| { const UnwrappedValue, const optional = switch (@typeInfo(field.type)) { .optional => |optional| .{ optional.child, true }, else => .{ field.type, false }, }; // Slice of strings is difficult to handle, so it gets special method. if (UnwrappedValue == [][]const u8) { const appender = struct { fn append(ptr: *T, value: [*:0]const u8) callconv(.C) void { const copy = std.heap.c_allocator.dupe(u8, std.mem.span(value)) catch { std.log.err("Failed to copy string for {s}: out of memory", .{ field.name, }); return; }; const len = @field(@field(ptr, opts.field), field.name).len; const new_slice = std.heap.c_allocator.realloc( @field(@field(ptr, opts.field), field.name), len + 1, ) catch { std.log.err("Failed to resize {s}: out of memory", .{ field.name, }); std.heap.c_allocator.free(copy); return; }; new_slice[len] = copy; @field(@field(ptr, opts.field), field.name) = new_slice; } }; @export(&appender.append, .{ .name = std.fmt.comptimePrint("{s}_append_{s}", .{ T.cname, field.name }), }); continue; } const CValue = switch (@typeInfo(UnwrappedValue)) { .pointer => |pointer| switch (pointer.size) { .slice => [*:0]pointer.child, else => @compileError("Non-slice pointer is not supported."), }, else => UnwrappedValue, }; const setter = struct { fn set(ptr: *T, value: CValue) callconv(.C) void { switch (@typeInfo(UnwrappedValue)) { .pointer => |pointer| { switch (pointer.size) { .slice => { const copy = std.heap.c_allocator.dupe( pointer.child, std.mem.span(value), ) catch { std.log.debug("Failed to copy for {s} in {s}: out of memory", .{ field.name, T.cname, }); return; }; if (optional) { if (@field(@field(ptr, opts.field), field.name)) |current| { std.heap.c_allocator.free(current); } } @field(@field(ptr, opts.field), field.name) = copy; }, else => { @compileError("Non-slice pointer is not supported."); }, } }, else => { @field(@field(ptr, opts.field), field.name) = value; }, } } }; @export(&setter.set, .{ .name = std.fmt.comptimePrint("{s}_set_{s}", .{ T.cname, field.name }), }); } } return T; } pub const DeserializableOptions = struct { type_field: []const u8 = "Data", json_options: std.json.ParseOptions = .{ .ignore_unknown_fields = true, }, }; pub fn Deserializable(comptime T: type, opts: DeserializableOptions) type { const from_json = struct { fn from_json(ptr: [*:0]const u8) callconv(.C) ?*T { const result = std.json.parseFromSlice( @field(T, opts.type_field).Raw, std.heap.c_allocator, std.mem.span(ptr), opts.json_options, ) catch return null; defer result.deinit(); return T.new(@field(T, opts.type_field).init(result.value) catch return null) catch return null; } }; comptime { @export(&from_json.from_json, .{ .name = std.fmt.comptimePrint("{s}_from_json", .{T.cname}), }); } return T; }
-
-
-
@@ -14,8 +14,10 @@ // limitations under the License.// // SPDX-License-Identifier: Apache-2.0 const public_api = @import("./api.zig").public_api; comptime { _ = public_api; namespace Roon { namespace Browse { public errordomain ParseError { INVALID_BROWSE_ACTION, } } }
-
-
src/Roon/rc.zig (deleted)
-
@@ -1,121 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 //! Atomic Reference Counting based on example code on `std.atomic.Value`. //! https://ziglang.org/documentation/0.14.0/std/#std.atomic.Value const std = @import("std"); const Rc = extern struct { count: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), /// Increment reference count by one. pub fn ref(arc: *Rc) callconv(.C) void { _ = arc.count.fetchAdd(1, .monotonic); } /// Decrement reference count by one, returns `true` if no reference is alive. pub fn unref(arc: *Rc) callconv(.C) bool { if (arc.count.fetchSub(1, .release) == 1) { _ = arc.count.load(.acquire); return true; } else { return false; } } }; pub fn RefCounted(comptime T: type) type { const Wrapped = if (@typeInfo(T).@"struct".layout == .@"extern") extern struct { pub const cname = T.cname; pub const Data = T; rc: Rc = .{}, data: T, pub fn new(data: T) !*@This() { const self = try std.heap.c_allocator.create(@This()); self.* = .{ .data = data, }; self.rc.ref(); return self; } pub fn retain(ptr: *@This()) callconv(.C) *@This() { ptr.rc.ref(); return ptr; } pub fn release(ptr: *@This()) callconv(.C) void { if (ptr.rc.unref()) { if (@hasDecl(T, "deinit")) { T.deinit(&ptr.data); } std.heap.c_allocator.destroy(ptr); } } } else struct { pub const cname = T.cname; pub const Data = T; rc: Rc = .{}, data: T, pub fn new(data: T) !*@This() { const self = try std.heap.c_allocator.create(@This()); self.* = .{ .data = data, }; self.rc.ref(); return self; } pub fn retain(ptr: *@This()) callconv(.C) *@This() { ptr.rc.ref(); return ptr; } pub fn release(ptr: *@This()) callconv(.C) void { if (ptr.rc.unref()) { if (@hasDecl(T, "deinit")) { T.deinit(&ptr.data); } std.heap.c_allocator.destroy(ptr); } } }; comptime { @export(&Wrapped.retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{T.cname}), }); @export(&Wrapped.release, .{ .name = std.fmt.comptimePrint("{s}_release", .{T.cname}), }); } return Wrapped; }
-
-
src/Roon/services/browse.zig (deleted)
-
@@ -1,389 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const rc = @import("../rc.zig"); const json = @import("../json.zig"); const allocator = std.heap.c_allocator; pub const Hierarchy = enum(c_int) { pub const cname = "roon_browse_Hierarchy"; browse = 0, playlists = 1, settings = 2, internet_radio = 3, albums = 4, artists = 5, genres = 6, composers = 7, search = 8, }; pub const ItemHint = enum(c_int) { pub const cname = "roon_browse_ItemHint"; unknown = 0, action = 1, action_list = 2, list = 3, header = 4, pub fn jsonParseFromValue( _: std.mem.Allocator, value: std.json.Value, _: std.json.ParseOptions, ) std.json.ParseFromValueError!@This() { return switch (value) { .string => |v| { inline for (@typeInfo(@This()).@"enum".fields) |field| { if (std.mem.eql(u8, field.name, "unknown")) { continue; } if (std.mem.eql(u8, field.name, v)) { return @enumFromInt(field.value); } } return .unknown; }, else => .unknown, }; } }; pub const ItemInputPrompt = rc.RefCounted(extern struct { pub const cname = "roon_browse_ItemInputPrompt"; prompt: [*:0]const u8, action: [*:0]const u8, value: ?[*:0]const u8, is_password: bool, pub fn init(raw: Raw) !@This() { return .{ .prompt = (try allocator.dupeZ(u8, raw.prompt)).ptr, .action = (try allocator.dupeZ(u8, raw.action)).ptr, .value = if (raw.value) |value| (try allocator.dupeZ(u8, value)).ptr else null, .is_password = raw.is_password, }; } pub const Raw = struct { prompt: [:0]const u8, action: [:0]const u8, value: ?[:0]const u8 = null, is_password: bool = false, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.prompt)); allocator.free(std.mem.span(self.action)); if (self.value) |value| { allocator.free(std.mem.span(value)); } } }); pub const Item = rc.RefCounted(extern struct { pub const cname = "roon_browse_Item"; title: [*:0]const u8, subtitle: ?[*:0]const u8, image_key: ?[*:0]const u8, item_key: ?[*:0]const u8, hint: ItemHint, input_prompt: ?*ItemInputPrompt, pub fn init(raw: Raw) !@This() { return .{ .title = (try allocator.dupeZ(u8, raw.title)).ptr, .subtitle = if (raw.subtitle) |subtitle| (try allocator.dupeZ(u8, subtitle)).ptr else null, .image_key = if (raw.image_key) |image_key| (try allocator.dupeZ(u8, image_key)).ptr else null, .item_key = if (raw.item_key) |item_key| (try allocator.dupeZ(u8, item_key)).ptr else null, .hint = raw.hint, .input_prompt = if (raw.input_prompt) |input_prompt| try ItemInputPrompt.new(try .init(input_prompt)) else null, }; } pub const Raw = struct { title: [:0]const u8, subtitle: ?[:0]const u8 = null, image_key: ?[:0]const u8 = null, item_key: ?[:0]const u8 = null, hint: ItemHint = .unknown, input_prompt: ?ItemInputPrompt.Data.Raw = null, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.title)); if (self.subtitle) |subtitle| { allocator.free(std.mem.span(subtitle)); } if (self.image_key) |image_key| { allocator.free(std.mem.span(image_key)); } if (self.item_key) |item_key| { allocator.free(std.mem.span(item_key)); } if (self.input_prompt) |input_prompt| { input_prompt.release(); } } }); pub const ListHint = enum(c_int) { pub const cname = "roon_browse_ListHint"; unknown = 0, action_list = 1, pub fn jsonParseFromValue( _: std.mem.Allocator, value: std.json.Value, _: std.json.ParseOptions, ) std.json.ParseFromValueError!@This() { return switch (value) { .string => |v| { inline for (@typeInfo(@This()).@"enum".fields) |field| { if (std.mem.eql(u8, field.name, "unknown")) { continue; } if (std.mem.eql(u8, field.name, v)) { return @enumFromInt(field.value); } } return .unknown; }, else => .unknown, }; } }; pub const List = rc.RefCounted(extern struct { pub const cname = "roon_browse_List"; title: [*:0]const u8, count: usize, subtitle: ?[*:0]const u8, image_key: ?[*:0]const u8, level: usize, display_offset: i64, has_display_offset: bool, hint: ListHint, pub fn init(raw: Raw) !@This() { return .{ .title = (try allocator.dupeZ(u8, raw.title)).ptr, .count = raw.count, .subtitle = if (raw.subtitle) |subtitle| (try allocator.dupeZ(u8, subtitle)).ptr else null, .image_key = if (raw.image_key) |image_key| (try allocator.dupeZ(u8, image_key)).ptr else null, .level = raw.level, .display_offset = raw.display_offset orelse 0, .has_display_offset = raw.display_offset != null, .hint = raw.hint, }; } pub const Raw = struct { title: []const u8, count: usize = 0, subtitle: ?[]const u8 = null, image_key: ?[]const u8 = null, level: usize = 0, display_offset: ?i64 = null, hint: ListHint = .unknown, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.title)); if (self.subtitle) |subtitle| { allocator.free(std.mem.span(subtitle)); } if (self.image_key) |image_key| { allocator.free(std.mem.span(image_key)); } } }); pub const BrowseAction = enum(c_int) { pub const cname = "roon_browse_BrowseAction"; message = 0, none = 1, list = 2, replace_item = 3, remove_item = 4, }; pub const browse = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_browse_browse_Request"; hierarchy: Hierarchy, item_key: ?[]const u8 = null, input: ?[]const u8 = null, zone_or_output_id: ?[]const u8 = null, pop_all: ?bool = null, pop_levels: ?usize = null, refresh_list: ?bool = null, pub fn init(hierarchy: Hierarchy) @This() { return .{ .hierarchy = hierarchy }; } pub fn deinit(self: *const @This()) void { if (self.item_key) |item_key| { allocator.free(item_key); } if (self.input) |input| { allocator.free(input); } if (self.zone_or_output_id) |zone_or_output_id| { allocator.free(zone_or_output_id); } } }), .{}); export fn roon_browse_browse_Request_new(hierarchy: Hierarchy) callconv(.C) ?*Request { return Request.new(.init(hierarchy)) catch return null; } pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_browse_browse_Response"; pub const deserializable = true; action: BrowseAction, item: ?*Item, list: ?*List, message: ?[*:0]const u8, is_error: bool, pub fn init(raw: Raw) !@This() { return .{ .action = raw.action, .item = if (raw.item) |item| try Item.new(try .init(item)) else null, .list = if (raw.list) |list| try List.new(try .init(list)) else null, .message = if (raw.message) |message| (try allocator.dupeZ(u8, message)).ptr else null, .is_error = raw.is_error orelse false, }; } pub const Raw = struct { action: BrowseAction, item: ?Item.Data.Raw = null, list: ?List.Data.Raw = null, message: ?[]const u8 = null, is_error: ?bool = null, }; pub fn deinit(self: *const @This()) void { if (self.item) |item| { item.release(); } if (self.list) |list| { list.release(); } if (self.message) |message| { allocator.free(std.mem.span(message)); } } }), .{}); }; pub const load = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_browse_load_Request"; set_display_offset: ?i64 = null, level: ?usize = 0, offset: ?i64 = 0, count: ?usize = 0, hierarchy: Hierarchy, multi_session_key: ?[]const u8 = null, pub fn init(hierarchy: Hierarchy) @This() { return .{ .hierarchy = hierarchy }; } pub fn deinit(self: *const @This()) void { if (self.multi_session_key) |multi_session_key| { allocator.free(multi_session_key); } } }), .{}); export fn roon_browse_load_Request_new(hierarchy: Hierarchy) callconv(.C) ?*Request { return Request.new(.init(hierarchy)) catch return null; } pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_browse_load_Response"; pub const deserializable = true; items_ptr: [*]const *Item, items_len: usize, offset: i64, list: *List, pub fn init(raw: Raw) !@This() { const items = try allocator.alloc(*Item, raw.items.len); errdefer allocator.free(items); var i: usize = 0; errdefer { for (0..i) |_| { items[i].release(); } } for (raw.items) |item| { items[i] = try Item.new(try .init(item)); i += 1; } return .{ .items_ptr = items.ptr, .items_len = items.len, .offset = raw.offset, .list = try List.new(try .init(raw.list)), }; } pub fn deinit(self: *const @This()) void { for (self.items_ptr[0..self.items_len]) |item| { item.release(); } allocator.free(self.items_ptr[0..self.items_len]); } pub const Raw = struct { items: []const Item.Data.Raw, offset: i64 = 0, list: List.Data.Raw, }; }), .{}); };
-
-
src/Roon/services/registry.zig (deleted)
-
@@ -1,139 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const rc = @import("../rc.zig"); const json = @import("../json.zig"); const allocator = std.heap.c_allocator; pub const info = struct { pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_registry_info_Response"; pub const deserializable = true; core_id: [*:0]const u8, display_name: [*:0]const u8, display_version: [*:0]const u8, pub fn init(raw: Raw) !@This() { const core_id = try allocator.dupeZ(u8, raw.core_id); errdefer allocator.free(core_id); const display_name = try allocator.dupeZ(u8, raw.display_name); errdefer allocator.free(display_name); const display_version = try allocator.dupeZ(u8, raw.display_version); errdefer allocator.free(display_version); return .{ .core_id = core_id.ptr, .display_name = display_name.ptr, .display_version = display_version.ptr, }; } pub const Raw = struct { core_id: []const u8, display_name: []const u8, display_version: []const u8, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.display_version)); allocator.free(std.mem.span(self.display_name)); allocator.free(std.mem.span(self.core_id)); } }), .{}); }; pub const register = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_registry_register_Request"; extension_id: ?[]const u8 = null, display_name: ?[]const u8 = null, display_version: ?[]const u8 = null, publisher: ?[]const u8 = null, email: ?[]const u8 = null, token: ?[]const u8 = null, required_services: [][]const u8 = &.{}, optional_services: [][]const u8 = &.{}, provided_services: [][]const u8 = &.{}, pub fn init() @This() { return .{}; } pub fn deinit(self: *const @This()) void { inline for (@typeInfo(@This()).@"struct".fields) |field| { if (field.type == ?[]const u8) { if (@field(self, field.name)) |value| { allocator.free(value); } continue; } if (field.type == [][]const u8) { for (@field(self, field.name)) |service| { allocator.free(service); } allocator.free(@field(self, field.name)); continue; } @compileError("Unhandled field type."); } } }), .{}); export fn roon_registry_register_Request_new() callconv(.C) ?*Request { return Request.new(.init()) catch return null; } pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_registry_register_Response"; pub const deserializable = true; core_id: [*:0]const u8, token: [*:0]const u8, pub fn init(raw: Raw) !@This() { const core_id = try allocator.dupeZ(u8, raw.core_id); errdefer allocator.free(core_id); const token = try allocator.dupeZ(u8, raw.token); errdefer allocator.free(token); return .{ .core_id = core_id.ptr, .token = token.ptr, }; } pub const Raw = struct { core_id: []const u8, token: []const u8, }; pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.token)); allocator.free(std.mem.span(self.core_id)); } }), .{}); };
-
-
src/Roon/services/transport.zig (deleted)
-
@@ -1,626 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const rc = @import("../rc.zig"); const json = @import("../json.zig"); const allocator = std.heap.c_allocator; pub const NowPlaying = rc.RefCounted(extern struct { pub const cname = "roon_transport_NowPlaying"; has_seek_info: bool, seek_position: u64, length: u64, image_key: ?[*:0]const u8, one_line_1: [*:0]const u8, two_line_1: [*:0]const u8, two_line_2: ?[*:0]const u8, three_line_1: [*:0]const u8, three_line_2: ?[*:0]const u8, three_line_3: ?[*:0]const u8, pub const Raw = struct { seek_position: ?u64, length: ?u64, image_key: ?[]const u8 = null, one_line: struct { line1: []const u8, }, two_line: struct { line1: []const u8, line2: ?[]const u8 = null, }, three_line: struct { line1: []const u8, line2: ?[]const u8 = null, line3: ?[]const u8 = null, }, }; pub fn init(raw: Raw) !@This() { const image_key = if (raw.image_key) |slice| try allocator.dupeZ(u8, slice) else null; errdefer if (image_key) |slice| allocator.free(slice); const one_line_1 = try allocator.dupeZ(u8, raw.one_line.line1); errdefer allocator.free(one_line_1); const two_line_1 = try allocator.dupeZ(u8, raw.two_line.line1); errdefer allocator.free(two_line_1); const two_line_2 = if (raw.two_line.line2) |slice| try allocator.dupeZ(u8, slice) else null; errdefer if (two_line_2) |slice| allocator.free(slice); const three_line_1 = try allocator.dupeZ(u8, raw.three_line.line1); errdefer allocator.free(three_line_1); const three_line_2 = if (raw.three_line.line2) |slice| try allocator.dupeZ(u8, slice) else null; errdefer if (three_line_2) |slice| allocator.free(slice); const three_line_3 = if (raw.three_line.line3) |slice| try allocator.dupeZ(u8, slice) else null; errdefer if (three_line_3) |slice| allocator.free(slice); return .{ .has_seek_info = raw.length != null and raw.seek_position != null, .seek_position = raw.seek_position orelse 0, .length = raw.length orelse 0, .image_key = if (image_key) |slice| slice.ptr else null, .one_line_1 = one_line_1.ptr, .two_line_1 = two_line_1.ptr, .two_line_2 = if (two_line_2) |slice| slice.ptr else null, .three_line_1 = three_line_1.ptr, .three_line_2 = if (three_line_2) |slice| slice.ptr else null, .three_line_3 = if (three_line_3) |slice| slice.ptr else null, }; } pub fn deinit(self: *const @This()) void { if (self.three_line_3) |ptr| { allocator.free(std.mem.span(ptr)); } if (self.three_line_2) |ptr| { allocator.free(std.mem.span(ptr)); } allocator.free(std.mem.span(self.three_line_1)); if (self.two_line_2) |ptr| { allocator.free(std.mem.span(ptr)); } allocator.free(std.mem.span(self.two_line_1)); allocator.free(std.mem.span(self.one_line_1)); if (self.image_key) |ptr| { allocator.free(std.mem.span(ptr)); } } }); pub const PlaybackState = enum(c_int) { pub const cname = "roon_transport_PlaybackState"; playing, paused, loading, stopped, }; pub const OutputVolume = rc.RefCounted(extern struct { pub const cname = "roon_transport_OutputVolume"; type: [*:0]const u8, has_min: bool, min: f64, has_max: bool, max: f64, has_value: bool, value: f64, has_step: bool, step: f64, is_muted: bool, pub const Raw = struct { type: []const u8, min: ?f64 = null, max: ?f64 = null, value: ?f64 = null, step: ?f64 = null, is_muted: bool = false, }; pub fn init(raw: Raw) !@This() { const @"type" = try allocator.dupeZ(u8, raw.type); errdefer allocator.free(@"type"); return .{ .type = @"type".ptr, .has_min = raw.min != null, .min = raw.min orelse 0, .has_max = raw.max != null, .max = raw.max orelse 0, .has_value = raw.value != null, .value = raw.value orelse 0, .has_step = raw.step != null, .step = raw.step orelse 0, .is_muted = raw.is_muted, }; } pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.type)); } }); pub const Output = rc.RefCounted(extern struct { pub const cname = "roon_transport_Output"; output_id: [*:0]const u8, display_name: [*:0]const u8, volume: ?*OutputVolume, pub const Raw = struct { output_id: []const u8, display_name: []const u8, volume: ?OutputVolume.Data.Raw = null, }; pub fn init(raw: Raw) !@This() { const output_id = try allocator.dupeZ(u8, raw.output_id); errdefer allocator.free(output_id); const display_name = try allocator.dupeZ(u8, raw.display_name); errdefer allocator.free(display_name); const volume = if (raw.volume) |volume_raw| try OutputVolume.new(try .init(volume_raw)) else null; errdefer if (volume) |ptr| ptr.release(); return .{ .output_id = output_id.ptr, .display_name = display_name, .volume = volume, }; } pub fn deinit(self: *const @This()) void { if (self.volume) |volume| { volume.release(); } allocator.free(std.mem.span(self.display_name)); allocator.free(std.mem.span(self.output_id)); } }); pub const Zone = rc.RefCounted(extern struct { pub const cname = "roon_transport_Zone"; zone_id: [*:0]const u8, display_name: [*:0]const u8, outputs_ptr: [*]*Output, outputs_len: usize, now_playing: ?*NowPlaying, state: PlaybackState, is_next_allowed: bool, is_previous_allowed: bool, is_pause_allowed: bool, is_play_allowed: bool, is_seek_allowed: bool, pub const Raw = struct { zone_id: []const u8, display_name: []const u8, outputs: []const Output.Data.Raw, now_playing: ?NowPlaying.Data.Raw = null, state: PlaybackState, is_next_allowed: bool = false, is_previous_allowed: bool = false, is_pause_allowed: bool = false, is_play_allowed: bool = false, is_seek_allowed: bool = false, }; pub fn init(raw: Raw) !@This() { const zone_id = try allocator.dupeZ(u8, raw.zone_id); errdefer allocator.free(zone_id); const display_name = try allocator.dupeZ(u8, raw.display_name); errdefer allocator.free(display_name); const outputs = try allocator.alloc(*Output, raw.outputs.len); errdefer allocator.free(outputs); var i: usize = 0; errdefer { for (0..i) |j| { outputs[j].release(); } } for (raw.outputs) |output| { outputs[i] = try Output.new(try .init(output)); i += 1; } const now_playing = if (raw.now_playing) |n| try NowPlaying.new(try .init(n)) else null; errdefer if (now_playing) |n| n.release(); return .{ .zone_id = zone_id.ptr, .display_name = display_name.ptr, .outputs_ptr = outputs.ptr, .outputs_len = outputs.len, .now_playing = now_playing, .state = raw.state, .is_next_allowed = raw.is_next_allowed, .is_previous_allowed = raw.is_previous_allowed, .is_pause_allowed = raw.is_pause_allowed, .is_play_allowed = raw.is_play_allowed, .is_seek_allowed = raw.is_seek_allowed, }; } pub fn deinit(self: *const @This()) void { if (self.now_playing) |now_playing| { now_playing.release(); } for (self.outputs_ptr[0..self.outputs_len]) |output| { output.release(); } allocator.free(self.outputs_ptr[0..self.outputs_len]); allocator.free(std.mem.span(self.display_name)); allocator.free(std.mem.span(self.zone_id)); } }); pub const SeekChange = rc.RefCounted(extern struct { pub const cname = "roon_transport_SeekChange"; zone_id: [*:0]const u8, has_seek_position: bool, seek_position: u64, pub const Raw = struct { zone_id: []const u8, seek_position: ?u64 = null, }; pub fn init(raw: Raw) !@This() { const zone_id = try allocator.dupeZ(u8, raw.zone_id); errdefer allocator.free(zone_id); return .{ .zone_id = zone_id, .has_seek_position = raw.seek_position != null, .seek_position = raw.seek_position orelse 0, }; } pub fn deinit(self: *const @This()) void { allocator.free(std.mem.span(self.zone_id)); } }); pub const ControlType = enum(c_int) { pub const cname = "roon_transport_ControlType"; play, pause, playpause, stop, previous, next, }; pub const SeekMode = enum(c_int) { pub const cname = "roon_transport_SeekMode"; relative, absolute, }; pub const ChangeVolumeMode = enum(c_int) { pub const cname = "roon_transport_ChangeVolumeMode"; absolute, relative, relative_step, }; pub const SubscribeZoneChanges = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_transport_SubscribeZoneChanges_Request"; subscription_key: []const u8, pub fn init(subscription_id: []const u8) @This() { return .{ .subscription_key = subscription_id }; } pub fn deinit(self: *const @This()) void { allocator.free(self.subscription_key); } }), .{}); export fn roon_transport_SubscribeZoneChanges_Request_new( subscription_id: [*:0]const u8, ) callconv(.C) ?*Request { const copy = allocator.dupe(u8, std.mem.span(subscription_id)) catch { std.log.err("Unable to copy subscription_id: out of memory", .{}); return null; }; return Request.new(.init(copy)) catch { std.log.err("Unable to create SubscribeZoneChange request: out of memory", .{}); allocator.free(copy); return null; }; } pub const Response = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_transport_SubscribeZoneChanges_Response"; pub const deserializable = true; zones_ptr: [*]*Zone, zones_len: usize, pub const Raw = struct { zones: []const Zone.Data.Raw, }; pub fn init(raw: Raw) !@This() { const zones = try allocator.alloc(*Zone, raw.zones.len); errdefer allocator.free(zones); var i: usize = 0; errdefer { for (0..i) |j| { zones[j].release(); } } for (raw.zones) |zone| { zones[i] = try Zone.new(try .init(zone)); i += 1; } return .{ .zones_ptr = zones.ptr, .zones_len = zones.len, }; } pub fn deinit(self: *const @This()) void { for (self.zones_ptr[0..self.zones_len]) |zone| { zone.release(); } allocator.free(self.zones_ptr[0..self.zones_len]); } }), .{}); pub const Event = json.Deserializable(rc.RefCounted(extern struct { pub const cname = "roon_transport_SubscribeZoneChanges_Event"; pub const deserializable = true; removed_zone_ids_ptr: [*][*:0]const u8, removed_zone_ids_len: usize, added_zones_ptr: [*]*Zone, added_zones_len: usize, changed_zones_ptr: [*]*Zone, changed_zones_len: usize, seek_changes_ptr: [*]*SeekChange, seek_changes_len: usize, pub const Raw = struct { zones_removed: []const []const u8 = &.{}, zones_added: []const Zone.Data.Raw = &.{}, zones_changed: []const Zone.Data.Raw = &.{}, zones_seek_changed: []const SeekChange.Data.Raw = &.{}, }; pub fn init(raw: Raw) !@This() { const removed_zone_ids = try allocator.alloc([*:0]const u8, raw.zones_removed.len); errdefer allocator.free(removed_zone_ids); var duped_removed_zone_ids: usize = 0; errdefer { for (0..duped_removed_zone_ids) |i| { allocator.free(std.mem.span(removed_zone_ids[i])); } } for (raw.zones_removed, 0..) |id, i| { removed_zone_ids[i] = try allocator.dupeZ(u8, id); duped_removed_zone_ids = i; } const added_zones = try allocator.alloc(*Zone, raw.zones_added.len); errdefer allocator.free(added_zones); var retained_added_zones: usize = 0; errdefer { for (0..retained_added_zones) |i| { added_zones[i].release(); } } for (raw.zones_added, 0..) |zone, i| { added_zones[i] = try Zone.new(try .init(zone)); retained_added_zones = i; } const changed_zones = try allocator.alloc(*Zone, raw.zones_changed.len); errdefer allocator.free(changed_zones); var retained_changed_zones: usize = 0; errdefer { for (0..retained_changed_zones) |i| { changed_zones[i].release(); } } for (raw.zones_changed, 0..) |zone, i| { changed_zones[i] = try Zone.new(try .init(zone)); retained_changed_zones = i; } const seek_changes = try allocator.alloc(*SeekChange, raw.zones_seek_changed.len); errdefer allocator.free(seek_changes); var retained_seek_changes: usize = 0; errdefer { for (0..retained_seek_changes) |i| { seek_changes[i].release(); } } for (raw.zones_seek_changed, 0..) |seek_change, i| { seek_changes[i] = try SeekChange.new(try .init(seek_change)); retained_seek_changes = i; } return .{ .removed_zone_ids_ptr = removed_zone_ids.ptr, .removed_zone_ids_len = removed_zone_ids.len, .added_zones_ptr = added_zones.ptr, .added_zones_len = added_zones.len, .changed_zones_ptr = changed_zones.ptr, .changed_zones_len = changed_zones.len, .seek_changes_ptr = seek_changes.ptr, .seek_changes_len = seek_changes.len, }; } pub fn deinit(self: *const @This()) void { for (self.seek_changes_ptr[0..self.seek_changes_len]) |seek_change| { seek_change.release(); } allocator.free(self.seek_changes_ptr[0..self.seek_changes_len]); for (self.changed_zones_ptr[0..self.changed_zones_len]) |zone| { zone.release(); } allocator.free(self.changed_zones_ptr[0..self.changed_zones_len]); for (self.added_zones_ptr[0..self.added_zones_len]) |zone| { zone.release(); } allocator.free(self.added_zones_ptr[0..self.added_zones_len]); for (self.removed_zone_ids_ptr[0..self.removed_zone_ids_len]) |id| { allocator.free(std.mem.span(id)); } allocator.free(self.removed_zone_ids_ptr[0..self.removed_zone_ids_len]); } }), .{}); }; pub const Control = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_transport_Control_Request"; zone_or_output_id: []const u8, control: ControlType, pub fn init(zone_or_output_id: []const u8, control: ControlType) @This() { return .{ .zone_or_output_id = zone_or_output_id, .control = control, }; } pub fn deinit(self: *const @This()) void { allocator.free(self.zone_or_output_id); } }), .{}); export fn roon_transport_Control_Request_new( zone_or_output_id: [*:0]const u8, control: ControlType, ) callconv(.C) ?*Request { const copy = allocator.dupe(u8, std.mem.span(zone_or_output_id)) catch { std.log.err("Unable to copy zone_or_output_id: out of memory", .{}); return null; }; return Request.new(.init(copy, control)) catch { std.log.err("Unable to create Control request: out of memory", .{}); allocator.free(copy); return null; }; } }; pub const Seek = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_transport_Seek_Request"; zone_or_output_id: []const u8, how: SeekMode = .relative, seconds: i64 = 0, pub fn init(zone_or_output_id: []const u8) @This() { return .{ .zone_or_output_id = zone_or_output_id }; } pub fn deinit(self: *const @This()) void { allocator.free(self.zone_or_output_id); } }), .{}); export fn roon_transport_Seek_Request_new(zone_or_output_id: [*:0]const u8) callconv(.C) ?*Request { const copy = allocator.dupe(u8, std.mem.span(zone_or_output_id)) catch { std.log.err("Unable to copy zone_or_output_id: out of memory", .{}); return null; }; return Request.new(.init(copy)) catch { std.log.err("Unable to create Seek request: out of memory", .{}); allocator.free(copy); return null; }; } }; pub const ChangeVolume = struct { pub const Request = json.Serializable(rc.RefCounted(struct { pub const cname = "roon_transport_ChangeVolume_Request"; output_id: []const u8, how: ChangeVolumeMode = .relative, value: f64 = 0, pub fn init(output_id: []const u8) @This() { return .{ .output_id = output_id }; } pub fn deinit(self: *const @This()) void { allocator.free(self.output_id); } }), .{}); export fn roon_transport_ChangeVolume_Request_new(output_id: [*:0]const u8) callconv(.C) ?*Request { const copy = allocator.dupe(u8, std.mem.span(output_id)) catch { std.log.err("Unable to copy output_id: out of memory", .{}); return null; }; return Request.new(.init(copy)) catch { std.log.err("Unable to create ChangeVolume request: out of memory", .{}); allocator.free(copy); return null; }; } };
-
-
-
@@ -165,7 +165,7 @@ });} private async void load_page_async(bool pop, bool scroll_to_top) throws GLib.Error { var browse_req = new Roon.Browse.Browse.Request(hierarchy); var browse_req = new Roon.Browse.Browse.Request((Roon.Browse.Hierarchy) hierarchy); if (item != null) { browse_req.item_key = item.item.item_key; }
-
@@ -253,13 +253,7 @@ throw new BrowseWidgetLoadPageError.BROWSE_ERROR(@"Expected Success response, got $(res.meta.service).");} var body = new Moo.JsonBody.from_string(res.message, res.headers); var result = new Roon.Browse.Browse.Response.from_json(body.data); if (result == null) { throw new BrowseWidgetLoadPageError.BROWSE_ERROR("Failed to parse browse response."); } return result; return new Roon.Browse.Browse.Response.from_json(body.data); } private async Roon.Browse.Load.Response load_async(Roon.Browse.Load.Request request) throws GLib.Error {
-
@@ -270,13 +264,7 @@ throw new BrowseWidgetLoadPageError.BROWSE_ERROR(@"Expected Success response, got $(res.meta.service).");} var body = new Moo.JsonBody.from_string(res.message, res.headers); var result = new Roon.Browse.Load.Response.from_json(body.data); if (result == null) { throw new BrowseWidgetLoadPageError.BROWSE_ERROR("Failed to parse load response."); } return result; return new Roon.Browse.Load.Response.from_json(body.data); } } }
-
-
-
@@ -324,7 +324,7 @@ }if ( zone.now_playing != null && zone.now_playing.has_seek_info zone.now_playing.has_seek_position ) { seek.sensitive = true; seek.set_range(0, zone.now_playing.length);
-
-
-
@@ -71,7 +71,7 @@ value_control.visible = false;return; } if (value.volume.type == "incremental") { if (value.volume.volume_type == "incremental") { incremental_control.visible = true; value_control.visible = false; } else {
-
@@ -201,7 +201,7 @@private async void step_volume_async( Plac.Connection conn, ZoneOutputRowVolumeStepDirection direction ) throws GLib.Error { double step = output.volume == null || output.volume.type == "incremental" ? 1.0 : output.volume.step; double step = output.volume == null || output.volume.volume_type == "incremental" ? 1.0 : output.volume.step; var req = new Roon.Transport.ChangeVolume.Request(output.output_id); req.how = RELATIVE;
-
-
-
@@ -178,9 +178,9 @@ req.email = "pockawoooh@gmail.com";if (token != null && token != "") { req.token = token; } req.append_required_services("com.roonlabs.transport:2"); req.append_required_services("com.roonlabs.browse:1"); req.append_provided_services("com.roonlabs.ping:1"); req.required_services.append_val("com.roonlabs.transport:2"); req.required_services.append_val("com.roonlabs.browse:1"); req.provided_services.append_val("com.roonlabs.ping:1"); server.connect_async.begin(req, (obj, res) => { try {
-