twilight_lavalink/
http.rs

1//! Models to deserialize responses into and functions to create `http` crate
2//! requests.
3
4use http::{
5    header::{HeaderValue, AUTHORIZATION},
6    Error as HttpError, Request,
7};
8use percent_encoding::NON_ALPHANUMERIC;
9use serde::{Deserialize, Deserializer, Serialize};
10use std::net::{IpAddr, SocketAddr};
11
12/// The type of search result given.
13#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14#[non_exhaustive]
15#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
16pub enum LoadType {
17    /// Loading the results failed.
18    LoadFailed,
19    /// There were no matches.
20    NoMatches,
21    /// A playlist was found.
22    PlaylistLoaded,
23    /// Some results were found.
24    SearchResult,
25    /// A single track was found.
26    TrackLoaded,
27}
28
29/// A track within a search result.
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
31#[non_exhaustive]
32#[serde(rename_all = "camelCase")]
33pub struct Track {
34    /// Details about a track, such as the author and title.
35    pub info: TrackInfo,
36    /// The base64 track string that you use in the [`Play`] event.
37    ///
38    /// [`Play`]: crate::model::outgoing::Play
39    pub track: String,
40}
41
42/// Additional information about a track, such as the author.
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[non_exhaustive]
45#[serde(rename_all = "camelCase")]
46pub struct TrackInfo {
47    /// The name of the author, if provided.
48    pub author: Option<String>,
49    /// The identifier of the source of the track.
50    pub identifier: String,
51    /// Whether the source is seekable.
52    pub is_seekable: bool,
53    /// Whether the source is a stream.
54    pub is_stream: bool,
55    /// The length of the audio in milliseconds.
56    pub length: u64,
57    /// The position of the audio.
58    pub position: u64,
59    /// The title, if provided.
60    pub title: Option<String>,
61    /// The source URI of the track.
62    pub uri: String,
63}
64
65/// Information about a playlist from a search result.
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[non_exhaustive]
68#[serde(rename_all = "camelCase")]
69pub struct PlaylistInfo {
70    /// The name of the playlist, if available.
71    pub name: Option<String>,
72    /// The selected track within the playlist, if available.
73    #[serde(default, deserialize_with = "deserialize_selected_track")]
74    pub selected_track: Option<u64>,
75}
76
77// Any negative value should be treated as None.
78fn deserialize_selected_track<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
79where
80    D: Deserializer<'de>,
81{
82    Ok(Option::<i64>::deserialize(deserializer)
83        .ok()
84        .flatten()
85        .and_then(|selected| u64::try_from(selected).ok()))
86}
87
88/// Possible track results for a query.
89#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
90#[non_exhaustive]
91#[serde(rename_all = "camelCase")]
92pub struct LoadedTracks {
93    /// The type of search result, such as a list of tracks or a playlist.
94    pub load_type: LoadType,
95    /// Information about the playlist, if provided.
96    pub playlist_info: PlaylistInfo,
97    /// The list of tracks returned for the search query.
98    pub tracks: Vec<Track>,
99}
100
101/// A failing IP address within the planner.
102#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
103#[non_exhaustive]
104#[serde(rename_all = "camelCase")]
105pub struct FailingAddress {
106    /// The IP address.
107    pub address: String,
108    /// The time that the address started failing in unix time.
109    pub failing_timestamp: u64,
110    /// The time that the address started failing as a timestamp.
111    pub failing_time: String,
112}
113
114/// The IP version in use by the block.
115#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
116#[non_exhaustive]
117pub enum IpBlockType {
118    /// An IPv4 block type.
119    #[serde(rename = "Inet4Address")]
120    Inet4,
121    /// An IPv6 block type.
122    #[serde(rename = "Inet6Address")]
123    Inet6,
124}
125
126/// A block of IP addresses.
127#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
128#[non_exhaustive]
129pub struct IpBlock {
130    /// The IP version of the IP block.
131    pub kind: IpBlockType,
132    /// The size of the block's addresses.
133    pub size: u64,
134}
135
136/// The type of route planner in use.
137#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
138#[non_exhaustive]
139#[serde(rename_all = "PascalCase")]
140pub enum RoutePlannerType {
141    /// A Nano IP route planner.
142    NanoIp,
143    /// A Rotating IP route planner.
144    RotatingIp,
145    /// A Rotating Nano IP route planner.
146    RotatingNanoIp,
147}
148
149/// The route planner in use.
150#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151#[non_exhaustive]
152#[serde(untagged)]
153pub enum RoutePlanner {
154    /// Information about a Nano IP route planner.
155    NanoIp(NanoIpRoutePlanner),
156    /// Information about a Rotating IP route planner.
157    RotatingIp(RotatingIpRoutePlanner),
158    /// Information about a Rotating Nano IP route planner.
159    RotatingNanoIp(RotatingNanoIpRoutePlanner),
160}
161
162/// A Nano IP planner.
163#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
164#[non_exhaustive]
165#[serde(rename_all = "camelCase")]
166pub struct NanoIpRoutePlanner {
167    /// The type of planner that is currently active.
168    ///
169    /// For this planner, this is always [`RoutePlannerType::NanoIp`]
170    pub class: RoutePlannerType,
171    /// The details of the currently active Nano IP route planner.
172    pub details: NanoIpDetails,
173}
174
175/// Information about a Nano IP planner.
176#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
177#[non_exhaustive]
178#[serde(rename_all = "camelCase")]
179pub struct NanoIpDetails {
180    /// The active offset within the IP block.
181    pub current_address_index: u64,
182    /// A list of IP addresses in the range that are failing.
183    pub failing_addresses: Vec<FailingAddress>,
184    /// The associated IP block.
185    pub ip_block: IpBlock,
186}
187
188/// A Rotating IP planner.
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190#[non_exhaustive]
191#[serde(rename_all = "camelCase")]
192pub struct RotatingIpRoutePlanner {
193    /// The type of planner that is currently active.
194    ///
195    /// For this planner, this is always [`RoutePlannerType::RotatingIp`]
196    pub class: RoutePlannerType,
197    /// The details of the currently active rotating IP route planner.
198    pub details: RotatingIpDetails,
199}
200
201/// Information about a Rotating IP planner.
202#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
203#[non_exhaustive]
204#[serde(rename_all = "camelCase")]
205pub struct RotatingIpDetails {
206    /// The currently used IP address.
207    pub current_address: String,
208    /// A list of IP addresses in the range that are failing.
209    pub failing_addresses: Vec<FailingAddress>,
210    /// The associated IP block.
211    pub ip_block: IpBlock,
212    /// The current offset used within the IP block.
213    pub ip_index: u64,
214    /// The number of rotations that have happened since the server started.
215    pub rotate_index: u64,
216}
217
218/// A Rotating Nano IP planner.
219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
220#[non_exhaustive]
221#[serde(rename_all = "camelCase")]
222pub struct RotatingNanoIpRoutePlanner {
223    /// The type of planner that is currently active.
224    ///
225    /// For this planner, this is always [`RoutePlannerType::RotatingNanoIp`]
226    pub class: RoutePlannerType,
227    /// The details of the currently active rotating nano IP route planner.
228    pub details: RotatingNanoIpDetails,
229}
230
231/// Information about a Rotating Nano IP planner.
232#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
233#[non_exhaustive]
234#[serde(rename_all = "camelCase")]
235pub struct RotatingNanoIpDetails {
236    /// The block IPs that are chosen.
237    pub block_index: String,
238    /// The current IP address on rotation.
239    pub current_address_index: u64,
240    /// A list of IP addresses in the range that are failing.
241    pub failing_addresses: Vec<FailingAddress>,
242    /// The associated IP block.
243    pub ip_block: IpBlock,
244}
245
246/// Get a list of tracks that match an identifier.
247///
248/// The response will include a body which can be deserialized into a
249/// [`LoadedTracks`].
250///
251/// # Errors
252///
253/// See the documentation for [`http::Error`].
254pub fn load_track(
255    address: SocketAddr,
256    identifier: impl AsRef<str>,
257    authorization: impl AsRef<str>,
258) -> Result<Request<&'static [u8]>, HttpError> {
259    let identifier =
260        percent_encoding::percent_encode(identifier.as_ref().as_bytes(), NON_ALPHANUMERIC);
261    let url = format!("http://{address}/loadtracks?identifier={identifier}");
262
263    let mut req = Request::get(url);
264
265    let auth_value = HeaderValue::from_str(authorization.as_ref())?;
266    req = req.header(AUTHORIZATION, auth_value);
267
268    req.body(b"")
269}
270
271/// Get the configured route planner for a node by address.
272///
273/// The response will include a body which can be deserialized into a
274/// [`RoutePlanner`].
275///
276/// # Errors
277///
278/// See the documentation for [`http::Error`].
279pub fn get_route_planner(
280    address: SocketAddr,
281    authorization: impl AsRef<str>,
282) -> Result<Request<&'static [u8]>, HttpError> {
283    let mut req = Request::get(format!("{address}/routeplanner/status"));
284
285    let auth_value = HeaderValue::from_str(authorization.as_ref())?;
286    req = req.header(AUTHORIZATION, auth_value);
287
288    req.body(b"")
289}
290
291/// Unmark an IP address as being failed, meaning that it can be used again.
292///
293/// The response will not include a body on success.
294///
295/// # Errors
296///
297/// See the documentation for [`http::Error`].
298#[allow(clippy::missing_panics_doc)]
299pub fn unmark_failed_address(
300    node_address: impl Into<SocketAddr>,
301    authorization: impl AsRef<str>,
302    route_address: impl Into<IpAddr>,
303) -> Result<Request<Vec<u8>>, HttpError> {
304    let mut req = Request::post(format!("{}/routeplanner/status", node_address.into()));
305
306    let auth_value = HeaderValue::from_str(authorization.as_ref())?;
307    req = req.header(AUTHORIZATION, auth_value);
308
309    req.body(
310        serde_json::to_vec(&serde_json::json!({
311            "address": route_address.into(),
312        }))
313        .expect("valid json"),
314    )
315}
316
317#[cfg(test)]
318mod tests {
319    use super::{
320        FailingAddress, IpBlock, IpBlockType, LoadType, LoadedTracks, NanoIpDetails,
321        NanoIpRoutePlanner, PlaylistInfo, RotatingIpDetails, RotatingIpRoutePlanner,
322        RotatingNanoIpDetails, RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track,
323        TrackInfo,
324    };
325    use serde::{Deserialize, Serialize};
326    use serde_test::Token;
327    use static_assertions::{assert_fields, assert_impl_all};
328    use std::fmt::Debug;
329
330    assert_fields!(FailingAddress: address, failing_timestamp, failing_time);
331    assert_impl_all!(
332        FailingAddress: Clone,
333        Debug,
334        Deserialize<'static>,
335        Eq,
336        PartialEq,
337        Send,
338        Serialize,
339        Sync,
340    );
341    assert_impl_all!(
342        IpBlockType: Clone,
343        Debug,
344        Deserialize<'static>,
345        Eq,
346        PartialEq,
347        Send,
348        Serialize,
349        Sync,
350    );
351    assert_fields!(IpBlock: kind, size);
352    assert_impl_all!(
353        IpBlock: Clone,
354        Debug,
355        Deserialize<'static>,
356        Eq,
357        PartialEq,
358        Send,
359        Serialize,
360        Sync,
361    );
362    assert_impl_all!(
363        LoadType: Clone,
364        Debug,
365        Deserialize<'static>,
366        Eq,
367        PartialEq,
368        Send,
369        Serialize,
370        Sync,
371    );
372    assert_fields!(LoadedTracks: load_type, playlist_info, tracks);
373    assert_impl_all!(
374        LoadedTracks: Clone,
375        Debug,
376        Deserialize<'static>,
377        Eq,
378        PartialEq,
379        Send,
380        Serialize,
381        Sync,
382    );
383    assert_fields!(
384        NanoIpDetails: current_address_index,
385        failing_addresses,
386        ip_block
387    );
388    assert_impl_all!(
389        NanoIpDetails: Clone,
390        Debug,
391        Deserialize<'static>,
392        Eq,
393        PartialEq,
394        Send,
395        Serialize,
396        Sync,
397    );
398    assert_fields!(NanoIpRoutePlanner: class, details);
399    assert_impl_all!(
400        NanoIpRoutePlanner: Clone,
401        Debug,
402        Deserialize<'static>,
403        Eq,
404        PartialEq,
405        Send,
406        Serialize,
407        Sync,
408    );
409    assert_fields!(PlaylistInfo: name, selected_track);
410    assert_impl_all!(
411        PlaylistInfo: Clone,
412        Debug,
413        Deserialize<'static>,
414        Eq,
415        PartialEq,
416        Send,
417        Serialize,
418        Sync,
419    );
420    assert_fields!(
421        RotatingIpDetails: current_address,
422        failing_addresses,
423        ip_block,
424        ip_index,
425        rotate_index
426    );
427    assert_impl_all!(
428        RotatingIpDetails: Clone,
429        Debug,
430        Deserialize<'static>,
431        Eq,
432        PartialEq,
433        Send,
434        Serialize,
435        Sync,
436    );
437    assert_fields!(RotatingIpRoutePlanner: class, details);
438    assert_impl_all!(
439        RotatingIpRoutePlanner: Clone,
440        Debug,
441        Deserialize<'static>,
442        Eq,
443        PartialEq,
444        Send,
445        Serialize,
446        Sync,
447    );
448    assert_fields!(
449        RotatingNanoIpDetails: block_index,
450        current_address_index,
451        failing_addresses,
452        ip_block
453    );
454    assert_impl_all!(
455        RotatingNanoIpDetails: Clone,
456        Debug,
457        Deserialize<'static>,
458        Eq,
459        PartialEq,
460        Send,
461        Serialize,
462        Sync,
463    );
464    assert_fields!(RotatingNanoIpRoutePlanner: class, details);
465    assert_impl_all!(
466        RotatingNanoIpRoutePlanner: Clone,
467        Debug,
468        Deserialize<'static>,
469        Eq,
470        PartialEq,
471        Send,
472        Serialize,
473        Sync,
474    );
475    assert_impl_all!(
476        RoutePlannerType: Clone,
477        Debug,
478        Deserialize<'static>,
479        Eq,
480        PartialEq,
481        Send,
482        Serialize,
483        Sync,
484    );
485    assert_impl_all!(
486        RoutePlanner: Clone,
487        Debug,
488        Deserialize<'static>,
489        Eq,
490        PartialEq,
491        Send,
492        Serialize,
493        Sync,
494    );
495    assert_fields!(
496        TrackInfo: author,
497        identifier,
498        is_seekable,
499        is_stream,
500        length,
501        position,
502        title,
503        uri
504    );
505    assert_impl_all!(
506        TrackInfo: Clone,
507        Debug,
508        Deserialize<'static>,
509        Eq,
510        PartialEq,
511        Send,
512        Serialize,
513        Sync
514    );
515    assert_fields!(Track: info, track);
516    assert_impl_all!(
517        Track: Clone,
518        Debug,
519        Deserialize<'static>,
520        Eq,
521        PartialEq,
522        Send,
523        Serialize,
524        Sync
525    );
526
527    #[test]
528    pub fn test_deserialize_playlist_info_negative_selected_track() {
529        let value = PlaylistInfo {
530            name: Some("Test Playlist".to_owned()),
531            selected_track: None,
532        };
533
534        serde_test::assert_de_tokens(
535            &value,
536            &[
537                Token::Struct {
538                    name: "PlaylistInfo",
539                    len: 13,
540                },
541                Token::Str("name"),
542                Token::Some,
543                Token::Str("Test Playlist"),
544                Token::Str("selectedTrack"),
545                Token::I64(-1),
546                Token::StructEnd,
547            ],
548        );
549    }
550}