1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
//! [Device API](https://gpoddernet.readthedocs.io/en/latest/api/reference/devices.html)

use crate::client::{AuthenticatedClient, DeviceClient};
use crate::episode::EpisodeActionType;
use crate::error::Error;
use crate::subscription::Podcast;
use chrono::naive::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::hash::{Hash, Hasher};
use url::Url;

/// Type of the [`Device`](./struct.Device.html)
#[serde(rename_all = "lowercase")]
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum DeviceType {
    /// desktop computer
    Desktop,
    /// portable computer
    Laptop,
    /// smartphone/tablet
    Mobile,
    /// server
    Server,
    /// any type of device, which doesn't fit another variant
    Other,
}

/// Devices are used throughout the API to identify a device / a client application.
#[derive(Deserialize, Serialize, Debug, Clone, Eq)]
pub struct Device {
    /// A device ID can be any string matching the regular expression `[\w.-]+`. The client application MUST generate a string to be used as its device ID, and SHOULD ensure that it is unique within the user account. A good approach is to combine the application name and the name of the host it is running on.
    ///
    /// If two applications share a device ID, this might cause subscriptions to be overwritten on the server side. While it is possible to retrieve a list of devices and their IDs from the server, this SHOULD NOT be used to let a user select an existing device ID.
    pub id: String,
    /// Human readable label for the device
    pub caption: String,
    /// Type of the device
    #[serde(rename(serialize = "type", deserialize = "type"))]
    pub device_type: DeviceType,
    /// number of subscriptions for this device
    pub subscriptions: u16,
}

#[derive(Serialize)]
pub(crate) struct DeviceData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) caption: Option<String>,
    #[serde(rename(serialize = "type", deserialize = "type"))]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) device_type: Option<DeviceType>,
}

/// episode update information as used in [DeviceUpdates](./struct.DeviceUpdates.html)
#[derive(Serialize, Deserialize)]
pub struct EpisodeUpdate {
    /// episode title
    pub title: String,
    /// episode URL
    pub url: Url,
    /// podcast title
    pub podcast_title: String,
    /// podcast URL
    pub podcast_url: Url,
    /// episode description
    pub description: String,
    /// episode website
    pub website: Url,
    /// gpodder.net internal URL
    pub mygpo_link: Url,
    /// episode release date
    pub released: NaiveDateTime,
    /// latest episode action reported for this episode
    pub status: Option<EpisodeActionType>,
}

/// updated information for a device as returned by [`get_device_updates`](./trait.GetDeviceUpdates.html#tymethod.get_device_updates)
#[derive(Serialize, Deserialize)]
pub struct DeviceUpdates {
    /// list of subscriptions to be added
    pub add: Vec<Podcast>,
    /// list of URLs to be unsubscribed
    pub rem: Vec<Url>,
    /// list of updated episodes
    pub updates: Vec<EpisodeUpdate>,
    /// current timestamp; for retrieving changes since the last query
    pub timestamp: u64,
}

/// see [`update_device_data`](./trait.UpdateDeviceData.html#tymethod.update_device_data)
pub trait UpdateDeviceData {
    /// Update Device Data
    ///
    /// # Parameters
    ///
    /// - `caption`: The new human readable label for the device
    /// - `device_type`: see [`DeviceType`](./enum.DeviceType.html)
    ///
    /// # Examples
    ///
    /// ```
    /// use mygpoclient::client::DeviceClient;
    /// use mygpoclient::device::{DeviceType,UpdateDeviceData};
    ///
    /// # let username = std::env::var("GPODDER_NET_USERNAME").unwrap();
    /// # let password = std::env::var("GPODDER_NET_PASSWORD").unwrap();
    /// # let deviceid = std::env::var("GPODDER_NET_DEVICEID").unwrap();
    /// #
    /// let client = DeviceClient::new(&username, &password, &deviceid);
    ///
    /// client.update_device_data("My Phone".to_owned(), DeviceType::Mobile)?;
    /// # Ok::<(), mygpoclient::error::Error>(())
    /// ```
    ///
    /// # See also
    ///
    /// - [gpodder.net API Documentation](https://gpoddernet.readthedocs.io/en/latest/api/reference/devices.html#update-device-data)
    fn update_device_data<T: Into<Option<String>>, U: Into<Option<DeviceType>>>(
        &self,
        caption: T,
        device_type: U,
    ) -> Result<(), Error>;
}

/// see [`list_devices`](./trait.ListDevices.html#tymethod.list_devices)
pub trait ListDevices {
    /// List Devices
    ///
    /// Returns the list of devices that belong to a user. This can be used by the client to let the user select a device from which to retrieve subscriptions, etc..
    ///
    /// # Examples
    ///
    /// ```
    /// use mygpoclient::client::AuthenticatedClient;
    /// use mygpoclient::device::ListDevices;
    ///
    /// # let username = std::env::var("GPODDER_NET_USERNAME").unwrap();
    /// # let password = std::env::var("GPODDER_NET_PASSWORD").unwrap();
    /// #
    /// let client = AuthenticatedClient::new(&username, &password);
    ///
    /// let devices = client.list_devices()?;
    ///
    /// # Ok::<(), mygpoclient::error::Error>(())
    /// ```
    ///
    /// # See also
    ///
    /// - [gpodder.net API Documentation](https://gpoddernet.readthedocs.io/en/latest/api/reference/devices.html#list-devices)
    fn list_devices(&self) -> Result<Vec<Device>, Error>;
}

/// see [`get_device_updates`](./trait.GetDeviceUpdates.html#tymethod.get_device_updates)
pub trait GetDeviceUpdates {
    /// Get Device Updates
    ///
    /// # Examples
    ///
    /// ```
    /// use mygpoclient::client::DeviceClient;
    /// use mygpoclient::device::GetDeviceUpdates;
    /// # use std::time::{SystemTime, UNIX_EPOCH};
    ///
    /// # let username = std::env::var("GPODDER_NET_USERNAME").unwrap();
    /// # let password = std::env::var("GPODDER_NET_PASSWORD").unwrap();
    /// # let deviceid = std::env::var("GPODDER_NET_DEVICEID").unwrap();
    /// # let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - 86400;
    /// #
    /// let client = DeviceClient::new(&username, &password, &deviceid);
    ///
    /// let device_updates = client.get_device_updates(timestamp, true)?;
    ///
    /// # Ok::<(), mygpoclient::error::Error>(())
    /// ```
    ///
    /// # See also
    ///
    /// - [gpodder.net API Documentation](https://gpoddernet.readthedocs.io/en/latest/api/reference/devices.html#get-device-updates)
    fn get_device_updates(&self, since: u64, include_actions: bool)
        -> Result<DeviceUpdates, Error>;
}

impl UpdateDeviceData for DeviceClient {
    fn update_device_data<T: Into<Option<String>>, U: Into<Option<DeviceType>>>(
        &self,
        caption: T,
        device_type: U,
    ) -> Result<(), Error> {
        let input = DeviceData {
            caption: caption.into(),
            device_type: device_type.into(),
        };
        self.post(
            &format!(
                "https://gpodder.net/api/2/devices/{}/{}.json",
                self.authenticated_client.username, self.device_id
            ),
            &input,
        )?;
        Ok(())
    }
}

impl ListDevices for AuthenticatedClient {
    fn list_devices(&self) -> Result<Vec<Device>, Error> {
        Ok(self
            .get(&format!(
                "https://gpodder.net/api/2/devices/{}.json",
                self.username
            ))?
            .json()?)
    }
}

impl ListDevices for DeviceClient {
    fn list_devices(&self) -> Result<Vec<Device>, Error> {
        self.as_ref().list_devices()
    }
}

impl GetDeviceUpdates for DeviceClient {
    fn get_device_updates(
        &self,
        since: u64,
        include_actions: bool,
    ) -> Result<DeviceUpdates, Error> {
        let mut query_parameters: Vec<&(&str, &str)> = Vec::new();

        let since_string = since.to_string();
        let query_parameter_since = ("since", since_string.as_ref());
        query_parameters.push(&query_parameter_since);

        let include_actions_string = include_actions.to_string();
        let query_parameter_include_actions = ("include_actions", include_actions_string.as_ref());
        query_parameters.push(&query_parameter_include_actions);

        Ok(self
            .get_with_query(
                &format!(
                    "https://gpodder.net/api/2/updates/{}/{}.json",
                    self.authenticated_client.username, self.device_id
                ),
                &query_parameters,
            )?
            .json()?)
    }
}

impl fmt::Display for DeviceType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl PartialEq for Device {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Ord for Device {
    fn cmp(&self, other: &Self) -> Ordering {
        self.id.cmp(&other.id)
    }
}

impl PartialOrd for Device {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Hash for Device {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.id.hash(state);
    }
}

impl fmt::Display for Device {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {} (id={})", self.device_type, self.caption, self.id)
    }
}

#[cfg(test)]
mod tests {
    use super::{Device, DeviceType};
    use std::cmp::Ordering;
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    #[test]
    fn equal_device_means_equal_hash() {
        let device1 = Device {
            id: String::from("abcdef"),
            caption: String::from("gPodder on my Lappy"),
            device_type: DeviceType::Laptop,
            subscriptions: 27,
        };
        let device2 = Device {
            id: String::from("abcdef"),
            caption: String::from("unnamed"),
            device_type: DeviceType::Other,
            subscriptions: 1,
        };

        assert_eq!(device1, device2);
        assert_eq!(device1.partial_cmp(&device2), Some(Ordering::Equal));

        let mut hasher1 = DefaultHasher::new();
        device1.hash(&mut hasher1);

        let mut hasher2 = DefaultHasher::new();
        device2.hash(&mut hasher2);

        assert_eq!(hasher1.finish(), hasher2.finish());
    }

    #[test]
    fn not_equal_devices_have_non_equal_ordering() {
        let device1 = Device {
            id: String::from("abcdef"),
            caption: String::from("gPodder on my Lappy"),
            device_type: DeviceType::Laptop,
            subscriptions: 27,
        };
        let device2 = Device {
            id: String::from("phone-au90f923023.203f9j23f"),
            caption: String::from("My Phone"),
            device_type: DeviceType::Mobile,
            subscriptions: 5,
        };

        assert_ne!(device1, device2);
        assert_eq!(device1.partial_cmp(&device2), Some(Ordering::Less));

        let mut hasher1 = DefaultHasher::new();
        device1.hash(&mut hasher1);

        let mut hasher2 = DefaultHasher::new();
        device2.hash(&mut hasher2);

        assert_ne!(hasher1.finish(), hasher2.finish());
    }

    #[test]
    fn display() {
        let device = Device {
            id: String::from("abcdef"),
            caption: String::from("gPodder on my Lappy"),
            device_type: DeviceType::Laptop,
            subscriptions: 27,
        };

        assert_eq!(
            "Laptop gPodder on my Lappy (id=abcdef)".to_owned(),
            format!("{}", device)
        );
    }
}