crunchyroll_rs/
crunchyroll.rs

1//! Builder and access to the [`Crunchyroll`] struct which is required to make any action.
2
3use crate::enum_values;
4use reqwest::Client;
5use std::sync::Arc;
6
7enum_values! {
8    /// Enum of supported languages by Crunchyroll.
9    /// Crunchyroll lists the available languages in the following api results:
10    /// - <https://static.crunchyroll.com/config/i18n/v3/audio_languages.json>
11    /// - <https://static.crunchyroll.com/config/i18n/v3/timed_text_languages.json>
12    #[allow(non_camel_case_types)]
13    #[derive(Hash, Ord, PartialOrd)]
14    pub enum Locale {
15        ar_SA = "ar-SA"
16        ca_ES = "ca-ES"
17        de_DE = "de-DE"
18        en_IN = "en-IN"
19        en_US = "en-US"
20        es_419 = "es-419"
21        es_ES = "es-ES"
22        fr_FR = "fr-FR"
23        hi_IN = "hi-IN"
24        id_ID = "id-ID"
25        it_IT = "it-IT"
26        ja_JP = "ja-JP"
27        ko_KR = "ko-KR"
28        ms_MY = "ms-MY"
29        pl_PL = "pl-PL"
30        pt_BR = "pt-BR"
31        pt_PT = "pt-PT"
32        ru_RU = "ru-RU"
33        ta_IN = "ta-IN"
34        te_IN = "te-IN"
35        th_TH = "th-TH"
36        tr_TR = "tr-TR"
37        vi_VN = "vi-VN"
38        zh_CN = "zh-CN"
39        zh_HK = "zh-HK"
40        zh_TW = "zh-TW"
41    }
42}
43
44impl Locale {
45    /// All available locales.
46    pub const fn all() -> &'static [Locale] {
47        &[
48            Locale::ar_SA,
49            Locale::ca_ES,
50            Locale::de_DE,
51            Locale::en_IN,
52            Locale::en_US,
53            Locale::es_419,
54            Locale::es_ES,
55            Locale::fr_FR,
56            Locale::hi_IN,
57            Locale::id_ID,
58            Locale::it_IT,
59            Locale::ja_JP,
60            Locale::ko_KR,
61            Locale::ms_MY,
62            Locale::pl_PL,
63            Locale::pt_BR,
64            Locale::pt_PT,
65            Locale::ru_RU,
66            Locale::ta_IN,
67            Locale::te_IN,
68            Locale::th_TH,
69            Locale::tr_TR,
70            Locale::vi_VN,
71            Locale::zh_CN,
72            Locale::zh_CN,
73            Locale::zh_TW,
74        ]
75    }
76
77    /// Converts the locale into a (english) human-readable string.
78    pub fn to_human_readable(&self) -> &str {
79        match self {
80            Locale::ar_SA => "Arabic (Saudi Arabia)",
81            Locale::ca_ES => "Catalan",
82            Locale::de_DE => "German",
83            Locale::en_IN => "English (India)",
84            Locale::en_US => "English (US)",
85            Locale::es_419 => "Spanish (Latin America)",
86            Locale::es_ES => "Spanish (Spain)",
87            Locale::fr_FR => "French",
88            Locale::hi_IN => "Hindi",
89            Locale::id_ID => "Indonesian",
90            Locale::it_IT => "Italian",
91            Locale::ja_JP => "Japanese",
92            Locale::ko_KR => "Korean",
93            Locale::ms_MY => "Malay",
94            Locale::pl_PL => "Polish",
95            Locale::pt_BR => "Portuguese (Brazil)",
96            Locale::pt_PT => "Portuguese (Portugal)",
97            Locale::ru_RU => "Russian",
98            Locale::ta_IN => "Tamil",
99            Locale::te_IN => "Telugu",
100            Locale::th_TH => "Thai",
101            Locale::tr_TR => "Turkish",
102            Locale::vi_VN => "Vietnamese",
103            Locale::zh_CN => "Chinese (China)",
104            Locale::zh_HK => "Chinese (Cantonese)",
105            Locale::zh_TW => "Chinese (Mandarin)",
106            Locale::Custom(custom) => custom.as_str(),
107        }
108    }
109}
110
111enum_values! {
112    /// Maturity rating.
113    pub enum MaturityRating {
114        NotMature = "M2"
115        Mature = "M3"
116    }
117}
118
119/// Starting point of this whole library.
120#[derive(Clone, Debug)]
121pub struct Crunchyroll {
122    pub(crate) executor: Arc<Executor>,
123}
124
125impl Crunchyroll {
126    pub fn builder() -> CrunchyrollBuilder {
127        CrunchyrollBuilder::default()
128    }
129
130    /// Return the (cloned) [`Client`] which is internally used to make requests.
131    pub fn client(&self) -> Client {
132        self.executor.client.clone()
133    }
134
135    /// Check if the current used account has premium.
136    pub async fn premium(&self) -> bool {
137        self.executor.premium().await
138    }
139
140    /// Return the access token used to make requests. The token changes every 5 minutes, so you
141    /// might have to re-call this function if you have a long-living session where you need it.
142    pub async fn access_token(&self) -> String {
143        self.executor.session.read().await.access_token.clone()
144    }
145
146    /// Return the current session token. It can be used to log-in later with
147    /// [`CrunchyrollBuilder::login_with_refresh_token`] or [`CrunchyrollBuilder::login_with_etp_rt`].
148    pub async fn session_token(&self) -> SessionToken {
149        self.executor.session.read().await.session_token.clone()
150    }
151
152    /// Return the device identifier for the current session.
153    pub fn device_identifier(&self) -> DeviceIdentifier {
154        self.executor.details.device_identifier.clone()
155    }
156}
157
158mod auth {
159    use crate::error::{Error, check_request};
160    use crate::media::StreamPlatform;
161    use crate::{Crunchyroll, Locale, Request, Result};
162    use chrono::{DateTime, Duration, Utc};
163    use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
164    use reqwest::{Client, ClientBuilder, IntoUrl, RequestBuilder, header};
165    use serde::de::DeserializeOwned;
166    use serde::{Deserialize, Serialize};
167    use std::ops::Add;
168    use std::sync::Arc;
169    use tokio::sync::RwLock;
170
171    /// Stores if the refresh token or etp-rt cookie was used for login. Extract the token and use
172    /// it as argument in their associated function ([`CrunchyrollBuilder::login_with_refresh_token`]
173    /// or [`CrunchyrollBuilder::login_with_etp_rt`]) if you want to re-login into the account again.
174    #[derive(Clone, Debug)]
175    pub enum SessionToken {
176        RefreshToken(String),
177        EtpRt(String),
178        Anonymous,
179    }
180
181    /// Information about the device that creates a new session.
182    #[derive(Clone, Debug)]
183    pub struct DeviceIdentifier {
184        /// The device id, this is specific for every device type, but usually represented as UUID.
185        /// Using [`Uuid::new_v4`] for it works fine.
186        pub device_id: String,
187        /// Type of the device which issues the session, e.g. `ANDROIDTV` (recommended, this is on
188        /// par with the default user agent and [`CrunchyrollBuilder::stream_platform`]),
189        /// `Chrome on Windows`, `iPhone 15` or `SM-G980F` (Samsung Galaxy S20).
190        pub device_type: String,
191        /// Name of the device which issues the session. This may be empty, for example all session
192        /// that are created over the website have an empty name; when issues via the app, the name
193        /// is the name of your phone (which you can modify/set when you set up the phone).
194        pub device_name: Option<String>,
195    }
196
197    impl Default for DeviceIdentifier {
198        fn default() -> Self {
199            Self {
200                device_id: "0000-0000-0000-0000".to_string(),
201                device_type: "0000-0000-0000-0000".to_string(),
202                device_name: None,
203            }
204        }
205    }
206
207    #[derive(Debug, Default, Deserialize)]
208    #[cfg_attr(feature = "__test_strict", serde(deny_unknown_fields))]
209    #[cfg_attr(not(feature = "__test_strict"), serde(default))]
210    #[allow(dead_code)]
211    struct AuthResponse {
212        access_token: String,
213        /// Is [`None`] if generated via [`Executor::auth_anonymously`].
214        refresh_token: Option<String>,
215        expires_in: i32,
216        token_type: String,
217        scope: String,
218        country: String,
219        /// Is [`None`] if generated via [`Executor::auth_anonymously`].
220        account_id: Option<String>,
221        /// Is [`None`] if generated via [`Executor::auth_anonymously`].
222        profile_id: Option<String>,
223    }
224
225    #[derive(Clone, Debug)]
226    pub(crate) struct ExecutorSession {
227        pub(crate) token_type: String,
228        pub(crate) access_token: String,
229        pub(crate) session_token: SessionToken,
230        pub(crate) session_expire: DateTime<Utc>,
231    }
232
233    #[allow(dead_code)]
234    #[derive(Clone, Debug)]
235    pub(crate) struct ExecutorDetails {
236        pub(crate) locale: Locale,
237        pub(crate) preferred_audio_locale: Option<Locale>,
238        pub(crate) device_identifier: DeviceIdentifier,
239        pub(crate) stream_platform: StreamPlatform,
240        pub(crate) basic_auth_token: String,
241
242        /// The account id is wrapped in a [`Result`] since [`Executor::auth_anonymously`] /
243        /// [`CrunchyrollBuilder::login_anonymously`] doesn't return an account id and to prevent
244        /// writing error messages multiple times in functions which require the account id to be
245        /// set they can just get the id or return the fix set error message.
246        pub(crate) account_id: Result<String>,
247    }
248
249    #[cfg(feature = "experimental-stabilizations")]
250    /// Contains which fixes should be used to make the api more reliable as Crunchyroll does weird
251    /// stuff / delivers incorrect results.
252    #[derive(Clone, Debug)]
253    pub(crate) struct ExecutorFixes {
254        pub(crate) locale_name_parsing: bool,
255        pub(crate) season_number: bool,
256    }
257
258    /// Internal struct to execute all request with.
259    #[derive(Debug)]
260    pub struct Executor {
261        pub(crate) client: Client,
262
263        /// Must be a [`RwLock`] because `Executor` is always passed inside `Arc` which does not
264        /// allow direct changes to the struct.
265        pub(crate) session: RwLock<ExecutorSession>,
266
267        pub(crate) details: ExecutorDetails,
268
269        #[cfg(feature = "tower")]
270        pub(crate) middleware: Option<tokio::sync::Mutex<crate::internal::tower::Middleware>>,
271        #[cfg(feature = "experimental-stabilizations")]
272        pub(crate) fixes: ExecutorFixes,
273    }
274
275    impl Executor {
276        pub(crate) fn get<U: IntoUrl>(self: &Arc<Self>, url: U) -> ExecutorRequestBuilder {
277            ExecutorRequestBuilder::new(self.clone(), self.client.get(url))
278        }
279
280        pub(crate) fn post<U: IntoUrl>(self: &Arc<Self>, url: U) -> ExecutorRequestBuilder {
281            ExecutorRequestBuilder::new(self.clone(), self.client.post(url))
282        }
283
284        pub(crate) fn put<U: IntoUrl>(self: &Arc<Self>, url: U) -> ExecutorRequestBuilder {
285            ExecutorRequestBuilder::new(self.clone(), self.client.put(url))
286        }
287
288        pub(crate) fn patch<U: IntoUrl>(self: &Arc<Self>, url: U) -> ExecutorRequestBuilder {
289            ExecutorRequestBuilder::new(self.clone(), self.client.patch(url))
290        }
291
292        pub(crate) fn delete<U: IntoUrl>(self: &Arc<Self>, url: U) -> ExecutorRequestBuilder {
293            ExecutorRequestBuilder::new(self.clone(), self.client.delete(url))
294        }
295
296        pub(crate) async fn request<T: Request + DeserializeOwned>(
297            self: &Arc<Self>,
298            mut req: RequestBuilder,
299        ) -> Result<T> {
300            req = self.auth_req(req).await?;
301            req = req.header(header::CONTENT_TYPE, "application/json");
302
303            let mut resp: T = request(
304                &self.client,
305                req,
306                #[cfg(feature = "tower")]
307                self.middleware.as_ref(),
308            )
309            .await?;
310
311            resp.__set_executor(self.clone()).await;
312
313            Ok(resp)
314        }
315
316        pub(crate) async fn auth_req(
317            self: &Arc<Self>,
318            mut req: RequestBuilder,
319        ) -> Result<RequestBuilder> {
320            let mut session = self.session.write().await;
321            if session.session_expire <= Utc::now() {
322                let login_response = match &session.session_token {
323                    SessionToken::RefreshToken(refresh_token) => {
324                        Executor::auth_with_refresh_token(
325                            &self.client,
326                            refresh_token.as_str(),
327                            &self.details.device_identifier,
328                            &self.details.basic_auth_token,
329                            #[cfg(feature = "tower")]
330                            self.middleware.as_ref(),
331                        )
332                        .await?
333                    }
334                    SessionToken::EtpRt(etp_rt) => {
335                        Executor::auth_with_etp_rt(
336                            &self.client,
337                            etp_rt.as_str(),
338                            &self.details.device_identifier,
339                            #[cfg(feature = "tower")]
340                            self.middleware.as_ref(),
341                        )
342                        .await?
343                    }
344                    SessionToken::Anonymous => {
345                        Executor::auth_anonymously(
346                            &self.client,
347                            &self.details.device_identifier,
348                            #[cfg(feature = "tower")]
349                            self.middleware.as_ref(),
350                        )
351                        .await?
352                    }
353                };
354
355                *session = ExecutorSession {
356                    token_type: login_response.token_type,
357                    access_token: login_response.access_token,
358                    session_token: match session.session_token {
359                        SessionToken::RefreshToken(_) => {
360                            SessionToken::RefreshToken(login_response.refresh_token.unwrap())
361                        }
362                        SessionToken::EtpRt(_) => {
363                            SessionToken::EtpRt(login_response.refresh_token.unwrap())
364                        }
365                        SessionToken::Anonymous => SessionToken::Anonymous,
366                    },
367                    session_expire: Utc::now()
368                        .add(Duration::try_seconds(login_response.expires_in as i64).unwrap()),
369                };
370            }
371
372            req = req.header(
373                header::AUTHORIZATION,
374                format!("{} {}", session.token_type, session.access_token),
375            );
376            Ok(req)
377        }
378
379        pub(crate) async fn jwt_claim<T: DeserializeOwned>(
380            &self,
381            claim: &str,
382        ) -> Result<Option<T>> {
383            let executor_session = self.session.read().await;
384
385            let token = executor_session.access_token.as_str();
386            // we just want the jwt claims, no need to check the signature. no safety critical
387            // processes rely on the jwt internally
388            let mut claims = jsonwebtoken::dangerous::insecure_decode::<
389                serde_json::Map<String, serde_json::Value>,
390            >(token)
391            .unwrap()
392            .claims;
393            if let Some(claim) = claims.remove(claim) {
394                Ok(serde_json::from_value(claim)?)
395            } else {
396                Ok(None)
397            }
398        }
399
400        pub(crate) async fn premium(&self) -> bool {
401            self.jwt_claim::<Vec<String>>("benefits")
402                .await
403                .unwrap()
404                .unwrap_or_default()
405                .contains(&"cr_premium".to_string())
406        }
407
408        fn auth_body<'a>(
409            mut pre_body: Vec<(&'a str, &'a str)>,
410            device_identifier: &'a DeviceIdentifier,
411        ) -> Vec<(&'a str, &'a str)> {
412            pre_body.push(("scope", "offline_access"));
413            pre_body.push(("device_id", device_identifier.device_id.as_str()));
414            pre_body.push(("device_type", device_identifier.device_type.as_str()));
415            if let Some(device_name) = &device_identifier.device_name {
416                pre_body.push(("device_name", device_name.as_str()));
417            }
418            pre_body
419        }
420
421        async fn auth_anonymously(
422            client: &Client,
423            device_identifier: &DeviceIdentifier,
424            #[cfg(feature = "tower")] middleware: Option<
425                &tokio::sync::Mutex<crate::internal::tower::Middleware>,
426            >,
427        ) -> Result<AuthResponse> {
428            let endpoint = "https://www.crunchyroll.com/auth/v1/token";
429            let body = Self::auth_body(vec![("grant_type", "client_id")], device_identifier);
430            let req = client
431                .post(endpoint)
432                .header(header::AUTHORIZATION, "Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=")
433                .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
434                .header("ETP-Anonymous-ID", &device_identifier.device_id)
435                .body(serde_urlencoded::to_string(body).unwrap())
436                .build()?;
437            #[cfg(not(feature = "tower"))]
438            let resp = client.execute(req).await?;
439            #[cfg(feature = "tower")]
440            let resp = {
441                use std::ops::DerefMut;
442                if let Some(middleware) = middleware {
443                    middleware.lock().await.deref_mut().call(req).await?
444                } else {
445                    client.execute(req).await?
446                }
447            };
448
449            check_request(endpoint.to_string(), resp).await
450        }
451
452        async fn auth_with_credentials(
453            client: &Client,
454            email: &str,
455            password: &str,
456            device_identifier: &DeviceIdentifier,
457            basic_auth_token: &str,
458            #[cfg(feature = "tower")] middleware: Option<
459                &tokio::sync::Mutex<crate::internal::tower::Middleware>,
460            >,
461        ) -> Result<AuthResponse> {
462            let endpoint = "https://www.crunchyroll.com/auth/v1/token";
463            let body = Self::auth_body(
464                vec![
465                    ("username", email),
466                    ("password", password),
467                    ("grant_type", "password"),
468                ],
469                device_identifier,
470            );
471            let req = client
472                .post(endpoint)
473                .header(header::AUTHORIZATION, format!("Basic {basic_auth_token}"))
474                .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
475                .header("ETP-Anonymous-ID", &device_identifier.device_id)
476                .body(serde_urlencoded::to_string(body).unwrap())
477                .build()?;
478            #[cfg(not(feature = "tower"))]
479            let resp = client.execute(req).await?;
480            #[cfg(feature = "tower")]
481            let resp = {
482                use std::ops::DerefMut;
483                if let Some(middleware) = middleware {
484                    middleware.lock().await.deref_mut().call(req).await?
485                } else {
486                    client.execute(req).await?
487                }
488            };
489
490            check_request(endpoint.to_string(), resp).await
491        }
492
493        async fn auth_with_refresh_token(
494            client: &Client,
495            refresh_token: &str,
496            device_identifier: &DeviceIdentifier,
497            basic_auth_token: &str,
498            #[cfg(feature = "tower")] middleware: Option<
499                &tokio::sync::Mutex<crate::internal::tower::Middleware>,
500            >,
501        ) -> Result<AuthResponse> {
502            let endpoint = "https://www.crunchyroll.com/auth/v1/token";
503            let body = Self::auth_body(
504                vec![
505                    ("refresh_token", refresh_token),
506                    ("grant_type", "refresh_token"),
507                ],
508                device_identifier,
509            );
510            let req = client
511                .post(endpoint)
512                .header(header::AUTHORIZATION, format!("Basic {basic_auth_token}"))
513                .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
514                .body(serde_urlencoded::to_string(body).unwrap())
515                .build()?;
516            #[cfg(not(feature = "tower"))]
517            let resp = client.execute(req).await?;
518            #[cfg(feature = "tower")]
519            let resp = {
520                use std::ops::DerefMut;
521                if let Some(middleware) = middleware {
522                    middleware.lock().await.deref_mut().call(req).await?
523                } else {
524                    client.execute(req).await?
525                }
526            };
527
528            check_request(endpoint.to_string(), resp).await
529        }
530
531        async fn auth_with_refresh_token_profile_id(
532            client: &Client,
533            refresh_token: &str,
534            profile_id: &str,
535            device_identifier: &DeviceIdentifier,
536            basic_auth_token: &str,
537            #[cfg(feature = "tower")] middleware: Option<
538                &tokio::sync::Mutex<crate::internal::tower::Middleware>,
539            >,
540        ) -> Result<AuthResponse> {
541            let endpoint = "https://www.crunchyroll.com/auth/v1/token";
542            let body = Self::auth_body(
543                vec![
544                    ("refresh_token", refresh_token),
545                    ("grant_type", "refresh_token_profile_id"),
546                    ("profile_id", profile_id),
547                ],
548                device_identifier,
549            );
550            let req = client
551                .post(endpoint)
552                .header(header::AUTHORIZATION, format!("Basic {basic_auth_token}"))
553                .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
554                .body(serde_urlencoded::to_string(body).unwrap())
555                .build()?;
556            #[cfg(not(feature = "tower"))]
557            let resp = client.execute(req).await?;
558            #[cfg(feature = "tower")]
559            let resp = {
560                use std::ops::DerefMut;
561                if let Some(middleware) = middleware {
562                    middleware.lock().await.deref_mut().call(req).await?
563                } else {
564                    client.execute(req).await?
565                }
566            };
567
568            check_request(endpoint.to_string(), resp).await
569        }
570
571        async fn auth_with_etp_rt(
572            client: &Client,
573            etp_rt: &str,
574            device_identifier: &DeviceIdentifier,
575            #[cfg(feature = "tower")] middleware: Option<
576                &tokio::sync::Mutex<crate::internal::tower::Middleware>,
577            >,
578        ) -> Result<AuthResponse> {
579            let endpoint = "https://www.crunchyroll.com/auth/v1/token";
580            let body = Self::auth_body(vec![("grant_type", "etp_rt_cookie")], device_identifier);
581            let req = client
582                .post(endpoint)
583                .header(header::AUTHORIZATION, "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6")
584                .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
585                .header(header::COOKIE, format!("etp_rt={etp_rt}"))
586                .body(serde_urlencoded::to_string(body).unwrap())
587                .build()?;
588            #[cfg(not(feature = "tower"))]
589            let resp = client.execute(req).await?;
590            #[cfg(feature = "tower")]
591            let resp = {
592                use std::ops::DerefMut;
593                if let Some(middleware) = middleware {
594                    middleware.lock().await.deref_mut().call(req).await?
595                } else {
596                    client.execute(req).await?
597                }
598            };
599
600            check_request(endpoint.to_string(), resp).await
601        }
602    }
603
604    impl Default for Executor {
605        fn default() -> Self {
606            Self {
607                client: Client::new(),
608                session: RwLock::new(ExecutorSession {
609                    token_type: "".to_string(),
610                    access_token: "".to_string(),
611                    session_token: SessionToken::RefreshToken("".into()),
612                    session_expire: Default::default(),
613                }),
614                details: ExecutorDetails {
615                    locale: Default::default(),
616                    preferred_audio_locale: None,
617                    device_identifier: DeviceIdentifier::default(),
618                    stream_platform: Default::default(),
619                    basic_auth_token: CrunchyrollBuilder::BASIC_AUTH_TOKEN.to_string(),
620                    account_id: Ok("".to_string()),
621                },
622                #[cfg(feature = "tower")]
623                middleware: None,
624                #[cfg(feature = "experimental-stabilizations")]
625                fixes: ExecutorFixes {
626                    locale_name_parsing: false,
627                    season_number: false,
628                },
629            }
630        }
631    }
632
633    pub(crate) struct ExecutorRequestBuilder {
634        executor: Arc<Executor>,
635        builder: RequestBuilder,
636    }
637
638    impl ExecutorRequestBuilder {
639        pub(crate) fn new(executor: Arc<Executor>, builder: RequestBuilder) -> Self {
640            Self { executor, builder }
641        }
642
643        pub(crate) fn query<T: Serialize + ?Sized>(mut self, query: &T) -> ExecutorRequestBuilder {
644            self.builder = self.builder.query(query);
645
646            self
647        }
648
649        pub(crate) fn apply_locale_query(self) -> ExecutorRequestBuilder {
650            let locale = self.executor.details.locale.clone();
651            self.query(&[("locale", locale)])
652        }
653
654        pub(crate) fn apply_preferred_audio_locale_query(self) -> ExecutorRequestBuilder {
655            if let Some(locale) = self.executor.details.preferred_audio_locale.clone() {
656                self.query(&[("preferred_audio_language", locale)])
657            } else {
658                self
659            }
660        }
661
662        pub(crate) fn apply_ratings_query(self) -> ExecutorRequestBuilder {
663            self.query(&[("ratings", "true")])
664        }
665
666        pub(crate) fn json<T: Serialize + ?Sized>(mut self, json: &T) -> ExecutorRequestBuilder {
667            self.builder = self.builder.json(json);
668
669            self
670        }
671
672        pub(crate) async fn request<T: Request + DeserializeOwned>(self) -> Result<T> {
673            self.executor.request(self.builder).await
674        }
675
676        pub(crate) async fn request_static<T: Request + DeserializeOwned>(
677            self,
678        ) -> Result<Option<T>> {
679            let raw_result = self.request_raw(false).await?;
680            if raw_result
681                .windows(8)
682                .any(move |window| window == b"</Error>")
683            {
684                Ok(None)
685            } else {
686                Ok(serde_json::from_slice(raw_result.as_slice())?)
687            }
688        }
689
690        pub(crate) async fn request_raw(mut self, auth: bool) -> Result<Vec<u8>> {
691            if auth {
692                self.builder = self.executor.auth_req(self.builder).await?;
693            }
694
695            #[cfg(feature = "tower")]
696            if let Some(middleware) = &self.executor.middleware {
697                return Ok(middleware
698                    .lock()
699                    .await
700                    .call(self.builder.build()?)
701                    .await?
702                    .bytes()
703                    .await?
704                    .to_vec());
705            }
706            Ok(self.builder.send().await?.bytes().await?.to_vec())
707        }
708    }
709
710    /// A builder to construct a new [`Crunchyroll`] instance. To create it, call
711    /// [`Crunchyroll::builder`].
712    pub struct CrunchyrollBuilder {
713        client: Client,
714        locale: Locale,
715        preferred_audio_locale: Option<Locale>,
716        stream_platform: StreamPlatform,
717        basic_auth_token: String,
718
719        #[cfg(feature = "tower")]
720        middleware: Option<tokio::sync::Mutex<crate::internal::tower::Middleware>>,
721        #[cfg(feature = "experimental-stabilizations")]
722        fixes: ExecutorFixes,
723    }
724
725    impl Default for CrunchyrollBuilder {
726        fn default() -> Self {
727            Self {
728                client: CrunchyrollBuilder::predefined_client_builder()
729                    .build()
730                    .unwrap(),
731                locale: Locale::en_US,
732                preferred_audio_locale: None,
733                stream_platform: StreamPlatform::default(),
734                basic_auth_token: CrunchyrollBuilder::BASIC_AUTH_TOKEN.to_string(),
735                #[cfg(feature = "tower")]
736                middleware: None,
737                #[cfg(feature = "experimental-stabilizations")]
738                fixes: ExecutorFixes {
739                    locale_name_parsing: false,
740                    season_number: false,
741                },
742            }
743        }
744    }
745
746    impl CrunchyrollBuilder {
747        pub const BASIC_AUTH_TOKEN: &'static str =
748            "Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=";
749        pub const USER_AGENT: &'static str = "Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 13.0; en-US; TCL-S5400AF Build/TP1A.220624.014)";
750
751        pub const DEFAULT_HEADERS: [(HeaderName, HeaderValue); 4] = [
752            (
753                header::USER_AGENT,
754                HeaderValue::from_static(CrunchyrollBuilder::USER_AGENT),
755            ),
756            (header::ACCEPT, HeaderValue::from_static("*/*")),
757            (
758                header::ACCEPT_LANGUAGE,
759                HeaderValue::from_static("en-US,en;q=0.5"),
760            ),
761            (header::CONNECTION, HeaderValue::from_static("keep-alive")),
762        ];
763
764        /// Return a [`ClientBuilder`] which has all required configurations necessary to send
765        /// successful requests to Crunchyroll, applied (most of the time; sometimes Crunchyroll has
766        /// fluctuations that requests doesn't work for a specific amount of time and after that
767        /// amount everything goes back to normal and works as it should). You can use this builder
768        /// to configure the behavior of the download client. Use [`CrunchyrollBuilder::client`] or
769        /// to set your built client.
770        pub fn predefined_client_builder() -> ClientBuilder {
771            let tls_config = rustls::ClientConfig::builder_with_provider(
772                rustls::crypto::CryptoProvider {
773                    cipher_suites: rustls::crypto::ring::DEFAULT_CIPHER_SUITES.to_vec(),
774                    kx_groups: vec![rustls::crypto::ring::kx_group::X25519],
775                    ..rustls::crypto::ring::default_provider()
776                }
777                .into(),
778            )
779            .with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
780            .unwrap()
781            .with_root_certificates(rustls::RootCertStore {
782                roots: webpki_roots::TLS_SERVER_ROOTS.into(),
783            })
784            .with_no_client_auth();
785
786            Client::builder()
787                .https_only(true)
788                .cookie_store(true)
789                .default_headers(HeaderMap::from_iter(CrunchyrollBuilder::DEFAULT_HEADERS))
790                .use_preconfigured_tls(tls_config)
791        }
792
793        /// Set a custom client that will be used in all api requests.
794        /// It is recommended to use the client builder from
795        /// [`CrunchyrollBuilder::predefined_client_builder`] as base as it has some configurations
796        /// which may be needed to make successful requests to Crunchyroll.
797        pub fn client(mut self, client: Client) -> CrunchyrollBuilder {
798            self.client = client;
799            self
800        }
801
802        /// Set in which languages all results which have human readable text in it should be
803        /// returned.
804        pub fn locale(mut self, locale: Locale) -> CrunchyrollBuilder {
805            self.locale = locale;
806            self
807        }
808
809        /// Set the audio language of media (like episodes) which should be returned when querying
810        /// by any other method than the direct media id. For example, if the preferred audio locale
811        /// were set to [`Locale::en_US`], the seasons queried with [`crate::Series::seasons`] would
812        /// likely have [`Locale::en_US`] as their audio locale. This might not always work on all
813        /// endpoints as Crunchyroll does Crunchyroll things (e.g. it seems to have no effect when
814        /// changing the locale and using [`Crunchyroll::query`]).
815        pub fn preferred_audio_locale(
816            mut self,
817            preferred_audio_locale: Locale,
818        ) -> CrunchyrollBuilder {
819            self.preferred_audio_locale = Some(preferred_audio_locale);
820            self
821        }
822
823        /// Set the platform for which a stream should be requested. The platform should match the
824        /// user agent, else requesting streams doesn't work. The user agent must be manually edited
825        /// by using [`CrunchyrollBuilder::client`] (you can use
826        /// [`CrunchyrollBuilder::predefined_client_builder`], update the user agent header and
827        /// the pass it to [`CrunchyrollBuilder::client`]).
828        pub fn stream_platform(mut self, stream_platform: StreamPlatform) -> CrunchyrollBuilder {
829            self.stream_platform = stream_platform;
830            self
831        }
832
833        /// Overwrite the basic auth token that is used to issue session. Crunchyroll rotates them
834        /// from time to time, which will result in failing logins.
835        /// This crate tries to keep the token up-to-date and push updates as soon as a new token is
836        /// available, but this doesn't always work. So in case such a case happens, or if you don't
837        /// want/can update to a newer crate version, you can use this method to overwrite said
838        /// token.
839        ///
840        /// Tools you can use to get new tokens:
841        /// - <https://github.com/crunchy-labs/crunchyroll-scripts>
842        pub fn basic_auth_token(mut self, basic_auth_token: String) -> CrunchyrollBuilder {
843            self.basic_auth_token = basic_auth_token;
844            self
845        }
846
847        /// Adds a [tower](https://docs.rs/tower/latest/tower/) middleware which is called on every
848        /// request.
849        #[cfg(feature = "tower")]
850        #[cfg_attr(docsrs, doc(cfg(feature = "tower")))]
851        pub fn middleware<F, S>(mut self, service: S) -> CrunchyrollBuilder
852        where
853            F: std::future::Future<Output = Result<reqwest::Response, Error>> + Send + 'static,
854            S: tower_service::Service<
855                    reqwest::Request,
856                    Response = reqwest::Response,
857                    Error = Error,
858                    Future = F,
859                > + Send
860                + 'static,
861        {
862            self.middleware = Some(tokio::sync::Mutex::new(
863                crate::internal::tower::Middleware::new(service),
864            ));
865            self
866        }
867
868        /// Set season and episode locales by parsing the season name and check if it contains
869        /// any language name.
870        /// Under special circumstances, this can slow down some methods as additional request must
871        /// be made. Currently, this applies to [`crate::Series`]. Whenever a request
872        /// is made which returns [`crate::Series`], internally [`crate::Series::seasons`] is called
873        /// for every series.
874        /// See <https://github.com/crunchy-labs/crunchyroll-rs/issues/3> for more information.
875        #[cfg(feature = "experimental-stabilizations")]
876        #[cfg_attr(docsrs, doc(cfg(feature = "experimental-stabilizations")))]
877        pub fn stabilization_locales(mut self, enable: bool) -> CrunchyrollBuilder {
878            self.fixes.locale_name_parsing = enable;
879            self
880        }
881
882        /// Set the season number of seasons by parsing a string which is delivered via the api too
883        /// and looks to be more reliable than the actual integer season number Crunchyroll provides.
884        #[cfg(feature = "experimental-stabilizations")]
885        #[cfg_attr(docsrs, doc(cfg(feature = "experimental-stabilizations")))]
886        pub fn stabilization_season_number(mut self, enable: bool) -> CrunchyrollBuilder {
887            self.fixes.season_number = enable;
888            self
889        }
890
891        /// Login without an account. This is just like if you would visit crunchyroll.com without
892        /// an account. Some functions won't work if logged in with this method.
893        pub async fn login_anonymously(
894            self,
895            device_identifier: DeviceIdentifier,
896        ) -> Result<Crunchyroll> {
897            self.pre_login().await?;
898
899            let login_response = Executor::auth_anonymously(
900                &self.client,
901                &device_identifier,
902                #[cfg(feature = "tower")]
903                self.middleware.as_ref(),
904            )
905            .await?;
906            let session_token = SessionToken::Anonymous;
907
908            self.post_login(login_response, session_token, device_identifier)
909                .await
910        }
911
912        /// Logs in with credentials (email and password) and returns a new [`Crunchyroll`] instance.
913        ///
914        /// *Note*: All logins you do with the generated refresh token must have the same
915        /// `device_identifier`, otherwise the login will fail.
916        pub async fn login_with_credentials<S: AsRef<str>>(
917            self,
918            email: S,
919            password: S,
920            device_identifier: DeviceIdentifier,
921        ) -> Result<Crunchyroll> {
922            self.pre_login().await?;
923
924            let login_response = Executor::auth_with_credentials(
925                &self.client,
926                email.as_ref(),
927                password.as_ref(),
928                &device_identifier,
929                &self.basic_auth_token,
930                #[cfg(feature = "tower")]
931                self.middleware.as_ref(),
932            )
933            .await?;
934            let session_token =
935                SessionToken::RefreshToken(login_response.refresh_token.clone().unwrap());
936
937            self.post_login(login_response, session_token, device_identifier)
938                .await
939        }
940
941        /// Logs in with a refresh token. This token is obtained when logging in with
942        /// [`CrunchyrollBuilder::login_with_credentials`].
943        ///
944        /// *Note*: Even though the tokens used in [`CrunchyrollBuilder::login_with_refresh_token`]
945        /// and [`CrunchyrollBuilder::login_with_etp_rt`] are having the same syntax, Crunchyroll
946        /// internal they're different. I had issues when I tried to log in with the refresh token
947        /// on [`CrunchyrollBuilder::login_with_etp_rt`] and vice versa.
948        ///
949        /// *Note*: You need to set the `device_identifier` to the same identifier which were used
950        /// in the login that initially created the refresh token, otherwise the login will fail.
951        pub async fn login_with_refresh_token<S: AsRef<str>>(
952            self,
953            refresh_token: S,
954            device_identifier: DeviceIdentifier,
955        ) -> Result<Crunchyroll> {
956            self.pre_login().await?;
957
958            let login_response = Executor::auth_with_refresh_token(
959                &self.client,
960                refresh_token.as_ref(),
961                &device_identifier,
962                &self.basic_auth_token,
963                #[cfg(feature = "tower")]
964                self.middleware.as_ref(),
965            )
966            .await?;
967            let session_token =
968                SessionToken::RefreshToken(login_response.refresh_token.clone().unwrap());
969
970            self.post_login(login_response, session_token, device_identifier)
971                .await
972        }
973
974        /// Just like [`CrunchyrollBuilder::login_with_refresh_token`] but with the addition that
975        /// the id of a [`crate::profile::Profile`] is given too. The resulting [`Crunchyroll`]
976        /// session will settings that are specific to the given [`crate::profile::Profile`] id.
977        ///
978        /// *Note*: When using this login method, some endpoints aren't available / will return an
979        /// error. Idk why, but these endpoints can only be used if the authentication is anything
980        /// other than [`CrunchyrollBuilder::login_with_refresh_token_profile_id`].
981        ///
982        /// *Note*: You need to set the `device_identifier` to the same identifier which were used
983        /// in the login that initially created the refresh token, otherwise the login will fail.
984        pub async fn login_with_refresh_token_profile_id<S: AsRef<str>>(
985            self,
986            refresh_token: S,
987            profile_id: S,
988            device_identifier: DeviceIdentifier,
989        ) -> Result<Crunchyroll> {
990            self.pre_login().await?;
991
992            let login_response = Executor::auth_with_refresh_token_profile_id(
993                &self.client,
994                refresh_token.as_ref(),
995                profile_id.as_ref(),
996                &device_identifier,
997                &self.basic_auth_token,
998                #[cfg(feature = "tower")]
999                self.middleware.as_ref(),
1000            )
1001            .await?;
1002            let session_token =
1003                SessionToken::RefreshToken(login_response.refresh_token.clone().unwrap());
1004
1005            self.post_login(login_response, session_token, device_identifier)
1006                .await
1007        }
1008
1009        /// Logs in with the `etp_rt` cookie that is generated when logging in with the browser and
1010        /// returns a new [`Crunchyroll`] instance. This cookie can be extracted if you copy the
1011        /// `etp_rt` cookie from your browser.
1012        ///
1013        /// *Note*: You need to set the `device_identifier` to the same identifier which were used
1014        /// in the login that initially created the `etp_rt` cookie, otherwise the login will fail.
1015        pub async fn login_with_etp_rt<S: AsRef<str>>(
1016            self,
1017            etp_rt: S,
1018            device_identifier: DeviceIdentifier,
1019        ) -> Result<Crunchyroll> {
1020            self.pre_login().await?;
1021
1022            let login_response = Executor::auth_with_etp_rt(
1023                &self.client,
1024                etp_rt.as_ref(),
1025                &device_identifier,
1026                #[cfg(feature = "tower")]
1027                self.middleware.as_ref(),
1028            )
1029            .await?;
1030            let session_token = SessionToken::EtpRt(login_response.refresh_token.clone().unwrap());
1031
1032            self.post_login(login_response, session_token, device_identifier)
1033                .await
1034        }
1035
1036        async fn pre_login(&self) -> Result<()> {
1037            // Request the index page to set cookies which are required to bypass the cloudflare bot
1038            // check
1039            self.client
1040                .get("https://www.crunchyroll.com")
1041                .send()
1042                .await?;
1043            Ok(())
1044        }
1045
1046        async fn post_login(
1047            self,
1048            login_response: AuthResponse,
1049            session_token: SessionToken,
1050            device_identifier: DeviceIdentifier,
1051        ) -> Result<Crunchyroll> {
1052            let crunchy = Crunchyroll {
1053                executor: Arc::new(Executor {
1054                    client: self.client,
1055
1056                    session: RwLock::new(ExecutorSession {
1057                        token_type: login_response.token_type,
1058                        access_token: login_response.access_token,
1059                        session_token,
1060                        session_expire: Utc::now()
1061                            .add(Duration::try_seconds(login_response.expires_in as i64).unwrap()),
1062                    }),
1063                    details: ExecutorDetails {
1064                        locale: self.locale,
1065                        preferred_audio_locale: self.preferred_audio_locale,
1066                        device_identifier,
1067                        stream_platform: self.stream_platform,
1068                        basic_auth_token: self.basic_auth_token,
1069
1070                        account_id: login_response.account_id.ok_or_else(|| {
1071                            Error::Authentication {
1072                                message: "Login with a user account to use this function"
1073                                    .to_string(),
1074                            }
1075                        }),
1076                    },
1077                    #[cfg(feature = "tower")]
1078                    middleware: self.middleware,
1079                    #[cfg(feature = "experimental-stabilizations")]
1080                    fixes: self.fixes,
1081                }),
1082            };
1083
1084            Ok(crunchy)
1085        }
1086    }
1087
1088    /// Make a request from the provided builder.
1089    async fn request<T: Request + DeserializeOwned>(
1090        client: &Client,
1091        req: RequestBuilder,
1092        #[cfg(feature = "tower")] middleware: Option<
1093            &tokio::sync::Mutex<crate::internal::tower::Middleware>,
1094        >,
1095    ) -> Result<T> {
1096        let built_req = req.build()?;
1097        let url = built_req.url().to_string();
1098        #[cfg(not(feature = "tower"))]
1099        let resp = client.execute(built_req).await?;
1100        #[cfg(feature = "tower")]
1101        let resp = {
1102            use std::ops::DerefMut;
1103            if let Some(middleware) = middleware {
1104                middleware.lock().await.deref_mut().call(built_req).await?
1105            } else {
1106                client.execute(built_req).await?
1107            }
1108        };
1109
1110        #[cfg(not(feature = "__test_strict"))]
1111        {
1112            check_request(url, resp).await
1113        }
1114        #[cfg(feature = "__test_strict")]
1115        {
1116            let result = check_request(url.clone(), resp).await?;
1117
1118            let cleaned = clean_request(result);
1119            // convert the map back to a string. by doing this, the error message contains the span
1120            // where the error occurred which improves debuggability
1121            let cleaned_string = serde_json::to_string(&cleaned).unwrap();
1122            serde_json::from_str(&cleaned_string).map_err(|e| Error::Decode {
1123                message: e.to_string(),
1124                content: cleaned_string.into_bytes(),
1125                url,
1126            })
1127        }
1128    }
1129
1130    /// Removes all fields which are starting and ending with `__` from a map (which is usually the
1131    /// response of a request). Some fields can be excluded from this process by providing the field
1132    /// names in `not_clean_fields`.
1133    #[cfg(feature = "__test_strict")]
1134    fn clean_request(
1135        mut map: serde_json::Map<String, serde_json::Value>,
1136    ) -> serde_json::Map<String, serde_json::Value> {
1137        for (key, value) in map.clone() {
1138            if key.starts_with("__") && key.ends_with("__") {
1139                map.remove(key.as_str());
1140            } else if let Some(object) = value.as_object() {
1141                map.insert(
1142                    key,
1143                    serde_json::to_value(clean_request(object.clone())).unwrap(),
1144                );
1145            } else if let Some(array) = value.as_array() {
1146                map.insert(
1147                    key,
1148                    serde_json::to_value(clean_request_array(array.clone())).unwrap(),
1149                );
1150            }
1151        }
1152        map
1153    }
1154
1155    #[cfg(feature = "__test_strict")]
1156    fn clean_request_array(mut arr: Vec<serde_json::Value>) -> Vec<serde_json::Value> {
1157        for (i, item) in arr.clone().iter().enumerate() {
1158            if let Some(object) = item.as_object() {
1159                arr[i] = serde_json::to_value(clean_request(object.clone())).unwrap();
1160            } else if let Some(array) = item.as_array() {
1161                arr[i] = serde_json::to_value(clean_request_array(array.clone())).unwrap();
1162            }
1163        }
1164        arr
1165    }
1166}
1167
1168pub(crate) use auth::Executor;
1169pub use auth::{CrunchyrollBuilder, DeviceIdentifier, SessionToken};