view src/beefweb.rs @ 6:482bd968725f default tip

beefweb: remove unnecessary (de)serialize derives
author Paper <paper@tflc.us>
date Mon, 06 Apr 2026 09:54:05 -0400
parents 26f695129c86
children
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/>.
*/

/* Fuck off, I don't care. */
#![allow(nonstandard_style)]

#[derive(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::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::Deserialize, Debug)]
pub enum VolumeType {
	#[serde(rename = "db")]
	DB,
	#[serde(rename = "linear")]
	LINEAR,
	#[serde(rename = "upDown")]
	UPDOWN, /* ??? */
}

#[derive(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::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::Deserialize, Debug)]
pub enum PlaybackState {
	#[serde(rename = "stopped")]
	STOPPED,
	#[serde(rename = "playing")]
	PLAYING,
	#[serde(rename = "paused")]
	PAUSED,
}

/* fb2k playback order */
#[derive(Debug)]
pub enum PlaybackOrder {
	DEFAULT,
	REPEAT_PLAYLIST,
	REPEAT_TRACK,
	RANDOM,
	SHUFFLE_TRACKS,
	SHUFFLE_ALBUMS,
	SHUFFLE_FOLDERS,
}

#[derive(serde::Deserialize, Debug)]
#[serde(tag = "type")]
pub enum Options {
	#[serde(rename = "enum")]
	Enumeration {
		#[serde(rename = "enumNames")]
		enum_names: Vec<String>,
		id: String,
		name: String,
		value: i64,
	},
	#[serde(rename = "bool")]
	Boolean {
		id: String,
		name: String,
		value: bool,
	},
}

#[derive(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: PlaybackState,
	pub volume: VolumeInfo,
	pub permissions: ApiPermissions,

	options: Option<Vec<Options>>,

	/* these two fields are deprecated,
	 * replaced with "options" array */
	#[serde(skip_serializing_if = "Option::is_none")]
	playbackMode: Option<i64>,

	#[serde(skip_serializing_if = "Option::is_none")]
	playbackModes: Option<Vec<String>>,
}

/* {"player": {struct Player}} */
#[derive(serde::Deserialize, Debug)]
struct PlayerHelper {
	player: Player,
}

#[derive(serde::Serialize, Debug)]
#[serde(untagged)]
enum SetPlayerOption {
    Integer { id: String, value: i64 },
    Boolean { id: String, value: bool },
}

#[derive(serde::Serialize, 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::Deserialize, Debug)]
pub struct PlaylistItem {
	pub columns: Vec<String>
}

#[derive(serde::Deserialize, Debug)]
pub struct PlaylistItems {
	pub offset: i64,
	#[serde(rename = "totalCount")]
	pub total_count: i64,
	pub items: Vec<PlaylistItem>,
}

#[derive(serde::Deserialize, Debug)]
struct PlaylistItemsHelper {
	#[serde(rename = "playlistItems")]
	playlist_items: PlaylistItems,
}

#[derive(serde::Deserialize, Debug)]
pub struct Playlist {
	pub id: String,
	pub index: i64,
	pub title: String,
	#[serde(rename = "isCurrent")]
	pub current: bool,
	#[serde(rename = "itemCount")]
	pub item_count: i64,
	#[serde(rename = "totalTime")]
	pub total_time: f64,
}

#[derive(serde::Deserialize, Debug)]
pub struct Playlists {
	pub playlists: Vec<Playlist>,
}

/* --- 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);
	}

	fn playback_order_lookup(playback_order: &str) -> Option<PlaybackOrder>
	{
		return match playback_order {
			"Default" => return Some(PlaybackOrder::DEFAULT),
			"Repeat (playlist)" => return Some(PlaybackOrder::REPEAT_PLAYLIST),
			"Repeat (track)" => return Some(PlaybackOrder::REPEAT_TRACK),
			"Random" => return Some(PlaybackOrder::RANDOM),
			"Shuffle (tracks)" => return Some(PlaybackOrder::SHUFFLE_TRACKS),
			"Shuffle (albums)" => return Some(PlaybackOrder::SHUFFLE_ALBUMS),
			"Shuffle (folders)" => return Some(PlaybackOrder::SHUFFLE_FOLDERS),
			_ => None,
		};
	}

	fn playback_order_cruft(v: &Vec<String>, s: i64) -> Option<PlaybackOrder>
	{
		let x = v.get(s as usize);

		match x {
			Some(y) => return Self::playback_order_lookup(y),
			_ => (),
		};

		return None;
	}

	pub async fn playback_order(&self) -> Result<PlaybackOrder, anyhow::Error>
	{
		let p = self.player().await?;

		match p.options {
			Some(x) => {
				for i in x {
					match i {
						Options::Enumeration { enum_names: e, id: id, name: _, value: v } => {
							match id.as_str() {
								"playbackOrder" => {
									match Self::playback_order_cruft(&e, v) {
										Some(z) => return Ok(z),
										_ => (),
									};
								},
								_ => (),
							};
						},
						_ => (),
					};
				}
			},
			_ => (),
		}

		match p.playbackModes {
			Some(x) => {
				match p.playbackMode {
					Some(y) => {
						match Self::playback_order_cruft(&x, y) {
							Some(v) => return Ok(v),
							_ => (),
						}
					},
					_ => (),
				};
			}
			_ => (),
		}

		return Err(anyhow::anyhow!("Unknown or invalid playback order?"));
	}

	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: None,
			muted: None,
			position: Some(position),
			relative_position: None,
			options: None,
		}).await;
	}

	pub async fn seek(&self, position: f64) -> Result<(), anyhow::Error>
	{
		return self.set_player(&SetPlayer {
			volume: None,
			relative_volume: None,
			muted: None,
			position: None,
			relative_position: Some(position),
			options: None,
		}).await;
	}

/* TODO -- finish this; need to link between string and enum

	pub async fn set_playback_order(&self, ) -> Result<(), anyhow::Error>
	{
		let p = self.set_player(&SetPlayer {
			volume: None,
			relative_volume: None,
			muted: None,
			position: None,
			relative_position: None,
			options: 
		})
	}
*/

	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 playlist_item(&self, playlist_id: &str, index: i64, columns: &Vec<&str>) -> Result<PlaylistItems, anyhow::Error>
	{
		return self.playlist_items(playlist_id, index, 1, columns).await;
	}

	pub async fn playlists(&self) -> Result<Vec<Playlist>, anyhow::Error>
	{
		return Ok(self.client
			.get(format!("{}/playlists", self.base_url))
			.send()
			.await?
			.json::<Playlists>()
			.await?
			.playlists);
	}

	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?);
	}
}