view src/beefweb.rs @ 1:a5ee18c79a04

license
author Paper <paper@tflc.us>
date Sat, 04 Apr 2026 12:34:46 -0400
parents d60ab8a4442f
children 594c0f9d7972
line wrap: on
line source

/*
 * Beefweb <-> mpris "compatibility" layer.
 *
 * 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?);
	}
}