diff src/beefweb.rs @ 0:d60ab8a4442f

*: check in
author Paper <paper@tflc.us>
date Sat, 04 Apr 2026 12:32:50 -0400
parents
children a5ee18c79a04
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/beefweb.rs	Sat Apr 04 12:32:50 2026 -0400
@@ -0,0 +1,270 @@
+/*
+ * Tiny layer for interfacing with beefweb. Does basically the
+ * bare minimum, and does not expose the whole API.
+ *
+ * Copyright (C) 2026 Paper
+ * 
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see
+ * <https://www.gnu.org/licenses/>.
+*/
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct PlayerInfo {
+	pub name: String,
+	pub title: String,
+	pub version: String,
+	// fuck camel case
+	#[serde(rename = "pluginVersion")]
+	pub plugin_version: String,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct ActiveItemInfo {
+	#[serde(rename = "playlistId")]
+	pub playlist_id: String,
+	#[serde(rename = "playlistIndex")]
+	pub playlist_index: i64,
+	pub index: i64,
+	pub position: f64,
+	pub duration: f64,
+	pub columns: Vec<String>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub enum VolumeType {
+	#[serde(rename = "db")]
+	DB,
+	#[serde(rename = "linear")]
+	LINEAR,
+	#[serde(rename = "upDown")]
+	UPDOWN, /* ??? */
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct VolumeInfo {
+	pub r#type: VolumeType,
+	pub min: f64,
+	pub max: f64,
+	pub value: f64,
+	#[serde(rename = "isMuted")]
+	pub muted: bool,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct ApiPermissions {
+	#[serde(rename = "changePlaylists")]
+	pub change_playlists: bool,
+	#[serde(rename = "changeOutput")]
+	pub change_output: bool,
+	#[serde(rename = "changeClientConfig")]
+	pub change_client_config: bool,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct Player {
+	pub info: PlayerInfo,
+	#[serde(rename = "activeItem")]
+	pub active_item: ActiveItemInfo,
+	/* XXX actually an enum */
+	#[serde(rename = "playbackState")]
+	pub playback_state: String,
+	pub volume: VolumeInfo,
+	pub permissions: ApiPermissions,
+}
+
+/* {"player": {struct Player}} */
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+struct PlayerHelper {
+	player: Player,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(untagged)]
+enum SetPlayerOption {
+    Integer { id: String, value: i64 },
+    Boolean { id: String, value: bool },
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde_with::skip_serializing_none]
+struct SetPlayer {
+	/* dB */
+	volume: Option<f64>,
+	/* ??? */
+	#[serde(rename = "relativeVolume")]
+	relative_volume: Option<f64>,
+	#[serde(rename = "isMuted")]
+	muted: Option<bool>,
+	position: Option<f64>,
+	#[serde(rename = "relativePosition")]
+	relative_position: Option<f64>,
+	/* teehee */
+	options: Option<Vec<SetPlayerOption>>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct PlaylistItem {
+	pub columns: Vec<String>
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct PlaylistItems {
+	pub offset: i64,
+	#[serde(rename = "totalCount")]
+	pub total_count: i64,
+	pub items: Vec<PlaylistItem>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+struct PlaylistItemsHelper {
+	#[serde(rename = "playlistItems")]
+	playlist_items: PlaylistItems,
+}
+
+/* --- NOW, THE ACTUAL BEEFWEB THING */
+
+pub struct Beefweb {
+	client: reqwest::Client,
+	base_url: String,
+}
+
+impl Beefweb {
+	pub fn new(base: &str) -> Beefweb
+	{
+		return Beefweb {
+			client: reqwest::Client::new(),
+			base_url: base.to_string(),
+		};
+	}
+
+	pub async fn player(&self) -> Result<Player, anyhow::Error>
+	{
+		return Ok(self.client
+			.get(format!("{}/player", self.base_url))
+			.send()
+			.await?
+			.json::<PlayerHelper>()
+			.await?
+			.player);
+	}
+
+	pub async fn volume(&self) -> Result<VolumeInfo, anyhow::Error>
+	{
+		return Ok(self.player().await?.volume);
+	}
+
+	pub async fn position(&self) -> Result<f64, anyhow::Error>
+	{
+		return Ok(self.player().await?.active_item.position);
+	}
+
+	/* XXX might be able to use macros for this? idk  --paper */
+	async fn play_pause_stop(&self, x: &str) -> Result<(), anyhow::Error>
+	{
+		self.client
+			.post(format!("{}/player/{}", self.base_url, x))
+			.send()
+			.await?;
+
+		return Ok(());
+	}
+
+	pub async fn play(&self) -> Result<(), anyhow::Error>
+	{
+		return self.play_pause_stop("play").await;
+	}
+
+	pub async fn pause(&self) -> Result<(), anyhow::Error>
+	{
+		return self.play_pause_stop("pause").await;
+	}
+
+	pub async fn stop(&self) -> Result<(), anyhow::Error>
+	{
+		return self.play_pause_stop("stop").await;
+	}
+
+	pub async fn play_pause(&self) -> Result<(), anyhow::Error>
+	{
+		return self.play_pause_stop("play-pause").await;
+	}
+
+	pub async fn next(&self) -> Result<(), anyhow::Error>
+	{
+		return self.play_pause_stop("next").await;
+	}
+
+	pub async fn previous(&self) -> Result<(), anyhow::Error>
+	{
+		return self.play_pause_stop("previous").await;
+	}
+
+	/* XXX what should this return? */
+	async fn set_player(&self, p: &SetPlayer) -> Result<(), anyhow::Error>
+	{
+		self.client
+			.post(format!("{}/player", self.base_url))
+			.json::<SetPlayer>(p)
+			.send()
+			.await?;
+
+		return Ok(());
+	}
+
+	pub async fn set_volume(&self, volume: f64) -> Result<(), anyhow::Error>
+	{
+		return self.set_player(&SetPlayer {
+			volume: Some(volume),
+			relative_volume: None,
+			muted: None,
+			position: None,
+			relative_position: None,
+			options: None,
+		}).await;
+	}
+
+	pub async fn set_position(&self, position: f64) -> Result<(), anyhow::Error>
+	{
+		return self.set_player(&SetPlayer {
+			volume: None,
+			relative_volume: Some(position),
+			muted: None,
+			position: None,
+			relative_position: None,
+			options: None,
+		}).await;
+	}
+
+	pub async fn playlist_items(&self, playlist_id: &str, offset: i64, count: i64, columns: Vec<&str>) -> Result<PlaylistItems, anyhow::Error>
+	{
+		return Ok(self.client
+			.get(format!("{}/playlists/{}/items/{}:{}", self.base_url, playlist_id, offset, count))
+			.query(&[("columns", columns.join(","))])
+			.send()
+			.await?
+			.json::<PlaylistItemsHelper>()
+			.await?
+			.playlist_items);
+	}
+
+	pub async fn artwork(&self, playlist_id: &str, index: i64) -> Result<bytes::Bytes, anyhow::Error>
+	{
+		return Ok(self.client
+			.get(format!("{}/artwork/{}/{}", self.base_url, playlist_id, index))
+			.send()
+			.await?
+			.bytes()
+			.await?);
+	}
+}