use http::{
header::{HeaderValue, AUTHORIZATION},
Error as HttpError, Request,
};
use percent_encoding::NON_ALPHANUMERIC;
use serde::{Deserialize, Deserializer, Serialize};
use std::net::{IpAddr, SocketAddr};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum LoadType {
LoadFailed,
NoMatches,
PlaylistLoaded,
SearchResult,
TrackLoaded,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Track {
pub info: TrackInfo,
pub track: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct TrackInfo {
pub author: Option<String>,
pub identifier: String,
pub is_seekable: bool,
pub is_stream: bool,
pub length: u64,
pub position: u64,
pub title: Option<String>,
pub uri: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct PlaylistInfo {
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_selected_track")]
pub selected_track: Option<u64>,
}
fn deserialize_selected_track<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<i64>::deserialize(deserializer)
.ok()
.flatten()
.and_then(|selected| u64::try_from(selected).ok()))
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct LoadedTracks {
pub load_type: LoadType,
pub playlist_info: PlaylistInfo,
pub tracks: Vec<Track>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct FailingAddress {
pub address: String,
pub failing_timestamp: u64,
pub failing_time: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub enum IpBlockType {
#[serde(rename = "Inet4Address")]
Inet4,
#[serde(rename = "Inet6Address")]
Inet6,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
pub struct IpBlock {
pub kind: IpBlockType,
pub size: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "PascalCase")]
pub enum RoutePlannerType {
NanoIp,
RotatingIp,
RotatingNanoIp,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(untagged)]
pub enum RoutePlanner {
NanoIp(NanoIpRoutePlanner),
RotatingIp(RotatingIpRoutePlanner),
RotatingNanoIp(RotatingNanoIpRoutePlanner),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct NanoIpRoutePlanner {
pub class: RoutePlannerType,
pub details: NanoIpDetails,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct NanoIpDetails {
pub current_address_index: u64,
pub failing_addresses: Vec<FailingAddress>,
pub ip_block: IpBlock,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct RotatingIpRoutePlanner {
pub class: RoutePlannerType,
pub details: RotatingIpDetails,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct RotatingIpDetails {
pub current_address: String,
pub failing_addresses: Vec<FailingAddress>,
pub ip_block: IpBlock,
pub ip_index: u64,
pub rotate_index: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct RotatingNanoIpRoutePlanner {
pub class: RoutePlannerType,
pub details: RotatingNanoIpDetails,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct RotatingNanoIpDetails {
pub block_index: String,
pub current_address_index: u64,
pub failing_addresses: Vec<FailingAddress>,
pub ip_block: IpBlock,
}
pub fn load_track(
address: SocketAddr,
identifier: impl AsRef<str>,
authorization: impl AsRef<str>,
) -> Result<Request<&'static [u8]>, HttpError> {
let identifier =
percent_encoding::percent_encode(identifier.as_ref().as_bytes(), NON_ALPHANUMERIC);
let url = format!("http://{address}/loadtracks?identifier={identifier}");
let mut req = Request::get(url);
let auth_value = HeaderValue::from_str(authorization.as_ref())?;
req = req.header(AUTHORIZATION, auth_value);
req.body(b"")
}
pub fn get_route_planner(
address: SocketAddr,
authorization: impl AsRef<str>,
) -> Result<Request<&'static [u8]>, HttpError> {
let mut req = Request::get(format!("{address}/routeplanner/status"));
let auth_value = HeaderValue::from_str(authorization.as_ref())?;
req = req.header(AUTHORIZATION, auth_value);
req.body(b"")
}
#[allow(clippy::missing_panics_doc)]
pub fn unmark_failed_address(
node_address: impl Into<SocketAddr>,
authorization: impl AsRef<str>,
route_address: impl Into<IpAddr>,
) -> Result<Request<Vec<u8>>, HttpError> {
let mut req = Request::post(format!("{}/routeplanner/status", node_address.into()));
let auth_value = HeaderValue::from_str(authorization.as_ref())?;
req = req.header(AUTHORIZATION, auth_value);
req.body(
serde_json::to_vec(&serde_json::json!({
"address": route_address.into(),
}))
.expect("valid json"),
)
}
#[cfg(test)]
mod tests {
use super::{
FailingAddress, IpBlock, IpBlockType, LoadType, LoadedTracks, NanoIpDetails,
NanoIpRoutePlanner, PlaylistInfo, RotatingIpDetails, RotatingIpRoutePlanner,
RotatingNanoIpDetails, RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track,
TrackInfo,
};
use serde::{Deserialize, Serialize};
use serde_test::Token;
use static_assertions::{assert_fields, assert_impl_all};
use std::fmt::Debug;
assert_fields!(FailingAddress: address, failing_timestamp, failing_time);
assert_impl_all!(
FailingAddress: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_impl_all!(
IpBlockType: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(IpBlock: kind, size);
assert_impl_all!(
IpBlock: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_impl_all!(
LoadType: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(LoadedTracks: load_type, playlist_info, tracks);
assert_impl_all!(
LoadedTracks: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(
NanoIpDetails: current_address_index,
failing_addresses,
ip_block
);
assert_impl_all!(
NanoIpDetails: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(NanoIpRoutePlanner: class, details);
assert_impl_all!(
NanoIpRoutePlanner: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(PlaylistInfo: name, selected_track);
assert_impl_all!(
PlaylistInfo: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(
RotatingIpDetails: current_address,
failing_addresses,
ip_block,
ip_index,
rotate_index
);
assert_impl_all!(
RotatingIpDetails: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(RotatingIpRoutePlanner: class, details);
assert_impl_all!(
RotatingIpRoutePlanner: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(
RotatingNanoIpDetails: block_index,
current_address_index,
failing_addresses,
ip_block
);
assert_impl_all!(
RotatingNanoIpDetails: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(RotatingNanoIpRoutePlanner: class, details);
assert_impl_all!(
RotatingNanoIpRoutePlanner: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_impl_all!(
RoutePlannerType: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_impl_all!(
RoutePlanner: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync,
);
assert_fields!(
TrackInfo: author,
identifier,
is_seekable,
is_stream,
length,
position,
title,
uri
);
assert_impl_all!(
TrackInfo: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync
);
assert_fields!(Track: info, track);
assert_impl_all!(
Track: Clone,
Debug,
Deserialize<'static>,
Eq,
PartialEq,
Send,
Serialize,
Sync
);
#[test]
pub fn test_deserialize_playlist_info_negative_selected_track() {
let value = PlaylistInfo {
name: Some("Test Playlist".to_owned()),
selected_track: None,
};
serde_test::assert_de_tokens(
&value,
&[
Token::Struct {
name: "PlaylistInfo",
len: 13,
},
Token::Str("name"),
Token::Some,
Token::Str("Test Playlist"),
Token::Str("selectedTrack"),
Token::I64(-1),
Token::StructEnd,
],
);
}
}