pandora-api 0.6.4

Low-level bindings to the (unofficial) Pandora web api.
Documentation
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
/*!
Authentication/authorization support messages.
*/
// SPDX-License-Identifier: MIT AND WTFPL

use std::collections::HashMap;

use pandora_api_derive::PandoraJsonRequest;
use serde::{Deserialize, Serialize};

use crate::errors::Error;
use crate::json::{PandoraJsonApiRequest, PandoraSession, ToPartnerTokens, ToUserTokens};

/// **Unsupported!**
/// Undocumented method
/// [auth.getAdMetadata()](https://6xq.net/pandora-apidoc/json/methods/)
pub struct GetAdMetadataUnsupported {}

/// **Unsupported!**
/// Undocumented method
/// [auth.partnerAdminLogin()](https://6xq.net/pandora-apidoc/json/methods/)
pub struct PartnerAdminLoginUnsupported {}

/// This request additionally serves as API version validation, time synchronization and endpoint detection and must be sent over a TLS-encrypted link. The POST body however is not encrypted.
///
/// | Name | Type | Description |
/// | username | string | See Partner passwords |
/// | password | string | See Partner passwords |
/// | deviceModel | string | See Partner passwords |
/// | version | string | Current version number, “5”. |
/// | includeUrls | boolean |  |
/// | returnDeviceType | boolean |  |
/// | returnUpdatePromptVersions | boolean |  |
/// ``` json
/// {
///     "username": "pandora one",
///     "password": "TVCKIBGS9AO9TSYLNNFUML0743LH82D",
///     "deviceModel": "D01",
///     "version": "5"
/// }
/// ```
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[serde(rename_all = "camelCase")]
pub struct PartnerLogin {
    /// The partner login name (not the account-holder's username)
    /// used to authenticate the application with the Pandora service.
    pub username: String,
    /// The partner login password (not the account-holder's username)
    /// used to authenticate the application with the Pandora service.
    pub password: String,
    /// The partner device model name.
    pub device_model: String,
    /// The Pandora JSON API version
    pub version: String,
    /// Optional parameters on the call
    #[serde(flatten)]
    pub optional: HashMap<String, serde_json::value::Value>,
}

impl PartnerLogin {
    /// Create a new PartnerLogin with some values. All Optional fields are
    /// set to None.
    pub fn new(
        username: &str,
        password: &str,
        device_model: &str,
        version: Option<String>,
    ) -> Self {
        PartnerLogin {
            username: username.to_string(),
            password: password.to_string(),
            device_model: device_model.to_string(),
            version: version.unwrap_or_else(|| String::from("5")),
            optional: HashMap::new(),
        }
    }

    /// Convenience function for setting boolean flags in the request. (Chaining call)
    pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
        self.optional
            .insert(option.to_string(), serde_json::value::Value::from(value));
        self
    }

    /// Whether to request to include urls in the response. (Chaining call)
    pub fn include_urls(self, value: bool) -> Self {
        self.and_boolean_option("includeUrls", value)
    }

    /// Whether to request to include the device type in the response. (Chaining call)
    pub fn return_device_type(self, value: bool) -> Self {
        self.and_boolean_option("returnDeviceType", value)
    }

    /// Whether to request to return a prompt to update versions in the response. (Chaining call)
    pub fn return_update_prompt_versions(self, value: bool) -> Self {
        self.and_boolean_option("returnUpdatePromptVersions", value)
    }

    /// This is a wrapper around the `response` method from the
    /// PandoraJsonApiRequest trait that automatically merges the partner tokens
    /// from the response back into the session.
    pub async fn merge_response(
        &self,
        session: &mut PandoraSession,
    ) -> Result<PartnerLoginResponse, Error> {
        let response = self.response(session).await?;
        session.update_partner_tokens(&response);
        Ok(response)
    }
}

/// syncTime is used to calculate the server time, see synctime. partnerId and authToken are required to proceed with user authentication.
///
/// | Name | Type | Description |
/// | syncTime | string | Hex-encoded, encrypted server time. Decrypt with password from Partner passwords and skip first four bytes of garbage. |
/// | partnerAuthToken | string |   |
/// | partnerId | string |   |
/// ``` json
/// {
///     "stat": "ok",
///     "result": {
///         "syncTime": "6923e263a8c3ac690646146b50065f43",
///         "deviceProperties": {
///             "videoAdRefreshInterval": 900,
///             "videoAdUniqueInterval": 0,
///             "adRefreshInterval": 5,
///             "videoAdStartInterval": 180
///         },
///         "partnerAuthToken": "VAzrFQTtsy3BQ3K+3iqFi0WF5HA63B1nFA",
///         "partnerId": "42",
///         "stationSkipUnit": "hour",
///         "urls": {
///             "autoComplete": "http://autocomplete.pandora.com/search"
///         },
///         "stationSkipLimit": 6
///     }
/// }
/// ```
/// | Code | Description |
/// | 1002 | INVALID_PARTNER_LOGIN. Invalid partner credentials. |
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PartnerLoginResponse {
    /// The partner id that should be used for this session
    pub partner_id: String,
    /// The partner auth token that should be used for this session
    pub partner_auth_token: String,
    /// The server sync time that should be used for this session
    /// Note that this field is encrypted, and must be decrypted before use
    pub sync_time: String,
    /// Unknown field
    pub station_skip_unit: String,
    /// Unknown field
    pub station_skip_limit: u32,
    /// Unknown field
    pub urls: Option<HashMap<String, String>>,
    /// Optional response fields
    #[serde(flatten)]
    pub optional: HashMap<String, serde_json::value::Value>,
}

impl ToPartnerTokens for PartnerLoginResponse {
    fn to_partner_id(&self) -> Option<String> {
        Some(self.partner_id.clone())
    }

    fn to_partner_token(&self) -> Option<String> {
        Some(self.partner_auth_token.clone())
    }

    fn to_sync_time(&self) -> Option<String> {
        Some(self.sync_time.clone())
    }
}

/// Convenience function to do a basic partnerLogin call.
pub async fn partner_login(
    session: &mut PandoraSession,
    username: &str,
    password: &str,
    device_model: &str,
) -> Result<PartnerLoginResponse, Error> {
    PartnerLogin::new(username, password, device_model, None)
        .include_urls(false)
        .return_device_type(false)
        .return_update_prompt_versions(false)
        .merge_response(session)
        .await
}

/// This request *must* be sent over a TLS-encrypted link. It authenticates the Pandora user by sending his username, usually his email address, and password as well as the partnerAuthToken obtained by Partner login.
///
/// Additional response data can be requested by setting flags listed below.
///
/// | Name | Type | Description |
/// | loginType | string | “user” |
/// | username | string | Username |
/// | password | string | User’s password |
/// | partnerAuthToken | string | Partner token obtained by Partner login |
/// | returnGenreStations | boolean | (optional) |
/// | returnCapped | boolean | return isCapped parameter (optional) |
/// | includePandoraOneInfo | boolean | (optional) |
/// | includeDemographics | boolean | (optional) |
/// | includeAdAttributes | boolean | (optional) |
/// | returnStationList | boolean | Return station list, see Retrieve station list (optional) |
/// | includeStationArtUrl | boolean | (optional) |
/// | includeStationSeeds | boolean | (optional) |
/// | includeShuffleInsteadOfQuickMix | boolean | (optional) |
/// | stationArtSize | string | W130H130(optional) |
/// | returnCollectTrackLifetimeStats | boolean | (optional) |
/// | returnIsSubscriber | boolean | (optional) |
/// | xplatformAdCapable | boolean | (optional) |
/// | complimentarySponsorSupported | boolean | (optional) |
/// | includeSubscriptionExpiration | boolean | (optional) |
/// | returnHasUsedTrial | boolean | (optional) |
/// | returnUserstate | boolean | (optional) |
/// | includeAccountMessage | boolean | (optional) |
/// | includeUserWebname | boolean | (optional) |
/// | includeListeningHours | boolean | (optional) |
/// | includeFacebook | boolean | (optional) |
/// | includeTwitter | boolean | (optional) |
/// | includeDailySkipLimit | boolean | (optional) |
/// | includeSkipDelay | boolean | (optional) |
/// | includeGoogleplay | boolean | (optional) |
/// | includeShowUserRecommendations | boolean | (optional) |
/// | includeAdvertiserAttributes | boolean | (optional) |
/// ``` json
/// {
///    "loginType": "user",
///    "username": "user@example.com",
///    "password": "example",
///    "partnerAuthToken": "VAzrFQTtsy3BQ3K+3iqFi0WF5HA63B1nFA",
///    "includePandoraOneInfo":true,
///    "includeAdAttributes":true,
///    "includeSubscriptionExpiration":true,
///    "includeStationArtUrl":true,
///    "returnStationList":true,
///    "returnGenreStations":true,
///    "syncTime": 1335777573
/// }
/// ```
#[derive(Debug, Clone, Serialize, PandoraJsonRequest)]
#[pandora_request(encrypted = true)]
#[serde(rename_all = "camelCase")]
pub struct UserLogin {
    /// This field should always have the value `user`.
    pub login_type: String,
    /// The account username to login with.
    pub username: String,
    /// The account password to login with.
    pub password: String,
    /// Optional parameters on the call
    #[serde(flatten)]
    pub optional: HashMap<String, serde_json::value::Value>,
}

impl UserLogin {
    /// Initialize a basic UserLogin request. All optional fields are set to None.
    pub fn new(username: &str, password: &str) -> Self {
        UserLogin {
            // This field should always have the value `user`.
            login_type: "user".to_string(),
            username: username.to_string(),
            password: password.to_string(),
            optional: HashMap::new(),
        }
    }

    /// Convenience function for setting boolean flags in the request. (Chaining call)
    pub fn and_boolean_option(mut self, option: &str, value: bool) -> Self {
        self.optional
            .insert(option.to_string(), serde_json::value::Value::from(value));
        self
    }

    /// Convenience function for setting string flags in the request. (Chaining call)
    pub fn and_string_option(mut self, option: &str, value: &str) -> Self {
        self.optional
            .insert(option.to_string(), serde_json::value::Value::from(value));
        self
    }

    /// Whether request should return genre stations in the response. (Chaining call)
    pub fn return_genre_stations(self, value: bool) -> Self {
        self.and_boolean_option("returnGenreStations", value)
    }

    /// Whether request should return capped in the response. (Chaining call)
    pub fn return_capped(self, value: bool) -> Self {
        self.and_boolean_option("returnCapped", value)
    }

    /// Whether request should include PandoraOne info in the response. (Chaining call)
    pub fn include_pandora_one_info(self, value: bool) -> Self {
        self.and_boolean_option("includePandoraOneInfo", value)
    }

    /// Whether request should include demographics in the response. (Chaining call)
    pub fn include_demographics(self, value: bool) -> Self {
        self.and_boolean_option("includeDemographics", value)
    }

    /// Whether request should include ad attributes in the response. (Chaining call)
    pub fn include_ad_attributes(self, value: bool) -> Self {
        self.and_boolean_option("includeAdAttributes", value)
    }

    /// Whether request should return station list in the response. (Chaining call)
    pub fn return_station_list(self, value: bool) -> Self {
        self.and_boolean_option("returnStationList", value)
    }

    /// Whether request should include the station art url in the response. (Chaining call)
    pub fn include_station_art_url(self, value: bool) -> Self {
        self.and_boolean_option("includeStationArtUrl", value)
    }

    /// Whether request should include the station seeds in the response. (Chaining call)
    pub fn include_station_seeds(self, value: bool) -> Self {
        self.and_boolean_option("includeStationSeeds", value)
    }

    /// Whether request should include shuffle stations instead of quickmix in the response. (Chaining call)
    pub fn include_shuffle_instead_of_quick_mix(self, value: bool) -> Self {
        self.and_boolean_option("includeShuffleInsteadOfQuickMix", value)
    }

    /// The size of station art to include in the response (if includeStationArlUrl was set). (Chaining call)
    pub fn station_art_size(self, value: &str) -> Self {
        self.and_string_option("stationArtSize", value)
    }

    /// Whether request should return collect track lifetime stats in the response. (Chaining call)
    pub fn return_collect_track_lifetime_stats(self, value: bool) -> Self {
        self.and_boolean_option("returnCollectTrackLifetimeStats", value)
    }

    /// Whether request should return whether the user is a subscriber in the response. (Chaining call)
    pub fn return_is_subscriber(self, value: bool) -> Self {
        self.and_boolean_option("returnIsSubscriber", value)
    }

    /// Whether the requesting client is cross-platform ad capable. (Chaining call)
    pub fn xplatform_ad_capable(self, value: bool) -> Self {
        self.and_boolean_option("xplatformAdCapable", value)
    }

    /// Whether the complimentary sponsors are supported. (Chaining call)
    pub fn complimentary_sponsor_supported(self, value: bool) -> Self {
        self.and_boolean_option("complimentarySponsorSupported", value)
    }

    /// Whether request should include subscription expiration in the response. (Chaining call)
    pub fn include_subscription_expiration(self, value: bool) -> Self {
        self.and_boolean_option("includeSubscriptionExpiration", value)
    }

    /// Whether request should return whether the user has used their trial
    /// subscription in the response. (Chaining call)
    pub fn return_has_used_trial(self, value: bool) -> Self {
        self.and_boolean_option("returnHasUsedTrial", value)
    }

    /// Whether request should return user state in the response. (Chaining call)
    pub fn return_userstate(self, value: bool) -> Self {
        self.and_boolean_option("returnUserstate", value)
    }

    /// Whether request should return account message in the response. (Chaining call)
    pub fn include_account_message(self, value: bool) -> Self {
        self.and_boolean_option("includeAccountMessage", value)
    }

    /// Whether request should include user webname in the response. (Chaining call)
    pub fn include_user_webname(self, value: bool) -> Self {
        self.and_boolean_option("includeUserWebname", value)
    }

    /// Whether request should include listening hours in the response. (Chaining call)
    pub fn include_listening_hours(self, value: bool) -> Self {
        self.and_boolean_option("includeListeningHours", value)
    }

    /// Whether request should include facebook connections in the response. (Chaining call)
    pub fn include_facebook(self, value: bool) -> Self {
        self.and_boolean_option("includeFacebook", value)
    }

    /// Whether request should include twitter connections in the response. (Chaining call)
    pub fn include_twitter(self, value: bool) -> Self {
        self.and_boolean_option("includeTwitter", value)
    }

    /// Whether request should include daily skip limit in the response. (Chaining call)
    pub fn include_daily_skip_limit(self, value: bool) -> Self {
        self.and_boolean_option("includeDailySkipLimit", value)
    }

    /// Whether request should include the track skip delay in the response. (Chaining call)
    pub fn include_skip_delay(self, value: bool) -> Self {
        self.and_boolean_option("includeSkipDelay", value)
    }

    /// Whether request should include Google Play metadata in the response. (Chaining call)
    pub fn include_googleplay(self, value: bool) -> Self {
        self.and_boolean_option("includeGoogleplay", value)
    }

    /// Whether request should include the user recommendations in the response. (Chaining call)
    pub fn include_show_user_recommendations(self, value: bool) -> Self {
        self.and_boolean_option("includeShowUserRecommendations", value)
    }

    /// Whether request should include advertiser attributes in the response. (Chaining call)
    pub fn include_advertiser_attributes(self, value: bool) -> Self {
        self.and_boolean_option("includeAdvertiserAttributes", value)
    }

    /// This is a wrapper around the `response` method from the
    /// PandoraJsonApiRequest trait that automatically merges the user tokens from
    /// the response back into the session.
    pub async fn merge_response(
        &self,
        session: &mut PandoraSession,
    ) -> Result<UserLoginResponse, Error> {
        let response = self.response(session).await?;
        session.update_user_tokens(&response);
        Ok(response)
    }
}

/// The returned userAuthToken is used to authenticate access to other API methods.
///
/// | Name | Type | Description |
/// | isCapped | boolean |  |
/// | userAuthToken | string |  |
/// ``` json
/// {
///    "stat": "ok",
///    "result": {
///        "stationCreationAdUrl": "http://ad.doubleclick.net/adx/pand.android/prod.createstation;ag=112;gnd=1;zip=23950;genre=0;model=;app=;OS=;dma=560;clean=0;logon=__LOGON__;tile=1;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*2jedvn446s7ce!ag*112!gnd*1!zip*23950!dma*560!clean*0!logon*__LOGON__!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!genre*0;sz=320x50;ord=__CACHEBUST__",
///        "hasAudioAds": true,
///        "splashScreenAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.welcome;ag=112;gnd=1;zip=23950;model=;app=;OS=;dma=560;clean=0;hours=1;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*op4jfgdxmddjk!ag*112!gnd*1!zip*23950!dma*560!clean*0!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!hours*1;sz=320x50;ord=__CACHEBUST__",
///        "videoAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.nowplaying;ag=112;gnd=1;zip=23950;dma=560;clean=0;hours=1;app=;index=__INDEX__;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*2jedvn446s7ce!ag*112!gnd*1!zip*23950!dma*560!clean*0!index*__INDEX__!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!hours*1;sz=442x188;ord=__CACHEBUST__",
///        "username": "user@example.com",
///        "canListen": true,
///        "nowPlayingAdUrl": "http://ad.doubleclick.net/pfadx/pand.android/prod.nowplaying;ag=112;gnd=1;zip=23950;genre=0;station={4};model=;app=;OS=;dma=560;clean=0;hours=1;artist=;interaction=__INTERACTION__;index=__INDEX__;newUser=__AFTERREG__;logon=__LOGON__;msa=115;st=VA;co=51117;et=0;mc=0;aa=0;hisp=0;hhi=0;u=l*op4jfgdxmddjk!ag*112!gnd*1!zip*23950!station*{4}!dma*560!clean*0!index*__INDEX__!newUser*__AFTERREG__!logon*__LOGON__!msa*115!st*VA!co*51117!et*0!mc*0!aa*0!hisp*0!hhi*0!genre*0!interaction*__INTERACTION__!hours*1;sz=320x50;ord=__CACHEBUST__",
///        "userId": "272772589",
///        "listeningTimeoutMinutes": "180",
///        "maxStationsAllowed": 100,
///        "listeningTimeoutAlertMsgUri": "/mobile/still_listening.vm",
///        "userProfileUrl": "https://www.pandora.com/login?auth_token=XXX&target=%2Fpeople%2FXXX",
///        "minimumAdRefreshInterval": 5,
///        "userAuthToken": "XXX"
///    }
/// }
/// ```
/// | Code | Description |
/// | 1002 | Wrong user credentials. |
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserLoginResponse {
    /// The user id that should be used for this session
    pub user_id: String,
    /// The user auth token that should be used for this session
    pub user_auth_token: String,
    /// Unknown field.
    pub station_creation_ad_url: String,
    /// Unknown field.
    pub has_audio_ads: bool,
    /// Unknown field.
    pub splash_screen_ad_url: String,
    /// Unknown field.
    pub video_ad_url: String,
    /// Unknown field.
    pub username: String,
    /// Unknown field.
    pub can_listen: bool,
    /// Unknown field.
    pub listening_timeout_minutes: String,
    /// Unknown field.
    pub max_stations_allowed: u32,
    /// Unknown field.
    pub listening_timeout_alert_msg_uri: String,
    /// Unknown field.
    pub user_profile_url: String,
    /// Unknown field.
    pub minimum_ad_refresh_interval: u32,
    /// Additional optional fields that may appear in the response.
    #[serde(flatten)]
    pub optional: HashMap<String, serde_json::value::Value>,
}

impl ToUserTokens for UserLoginResponse {
    fn to_user_id(&self) -> Option<String> {
        Some(self.user_id.clone())
    }

    fn to_user_token(&self) -> Option<String> {
        Some(self.user_auth_token.clone())
    }
}

/// Convenience function to perform a basic user login.
pub async fn user_login(
    session: &mut PandoraSession,
    username: &str,
    password: &str,
) -> Result<UserLoginResponse, Error> {
    UserLogin::new(username, password)
        .return_genre_stations(false)
        .return_capped(false)
        .include_pandora_one_info(false)
        .include_demographics(false)
        .include_ad_attributes(false)
        .return_station_list(false)
        .include_station_art_url(false)
        .include_station_seeds(false)
        .include_shuffle_instead_of_quick_mix(false)
        .return_collect_track_lifetime_stats(false)
        .return_is_subscriber(false)
        .xplatform_ad_capable(false)
        .complimentary_sponsor_supported(false)
        .include_subscription_expiration(false)
        .return_has_used_trial(false)
        .return_userstate(false)
        .include_account_message(false)
        .include_user_webname(false)
        .include_listening_hours(false)
        .include_facebook(false)
        .include_twitter(false)
        .include_daily_skip_limit(false)
        .include_skip_delay(false)
        .include_googleplay(false)
        .include_show_user_recommendations(false)
        .include_advertiser_attributes(false)
        .merge_response(session)
        .await
}

#[cfg(test)]
mod tests {
    use crate::json::{tests::session_login, Partner};

    // Tests both PartnerLogin and UserLogin
    #[tokio::test]
    async fn auth_test() {
        /*
        flexi_logger::Logger::try_with_str("info, pandora_api=debug")
            .expect("Failed to set logging configuration")
            .start()
            .expect("Failed to start logger");
        */

        let partner = Partner::default();
        let session = session_login(&partner)
            .await
            .expect("Failed initializing login session");
        log::debug!("Session tokens: {:?}", session);
    }
}