twilight_lavalink/
http.rs

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