1use crate::enum_values;
4use reqwest::Client;
5use std::sync::Arc;
6
7enum_values! {
8 #[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 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 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 pub enum MaturityRating {
114 NotMature = "M2"
115 Mature = "M3"
116 }
117}
118
119#[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 pub fn client(&self) -> Client {
132 self.executor.client.clone()
133 }
134
135 pub async fn premium(&self) -> bool {
137 self.executor.premium().await
138 }
139
140 pub async fn access_token(&self) -> String {
143 self.executor.session.read().await.access_token.clone()
144 }
145
146 pub async fn session_token(&self) -> SessionToken {
149 self.executor.session.read().await.session_token.clone()
150 }
151
152 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 #[derive(Clone, Debug)]
175 pub enum SessionToken {
176 RefreshToken(String),
177 EtpRt(String),
178 Anonymous,
179 }
180
181 #[derive(Clone, Debug)]
183 pub struct DeviceIdentifier {
184 pub device_id: String,
187 pub device_type: String,
191 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 refresh_token: Option<String>,
215 expires_in: i32,
216 token_type: String,
217 scope: String,
218 country: String,
219 account_id: Option<String>,
221 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 pub(crate) account_id: Result<String>,
247 }
248
249 #[cfg(feature = "experimental-stabilizations")]
250 #[derive(Clone, Debug)]
253 pub(crate) struct ExecutorFixes {
254 pub(crate) locale_name_parsing: bool,
255 pub(crate) season_number: bool,
256 }
257
258 #[derive(Debug)]
260 pub struct Executor {
261 pub(crate) client: Client,
262
263 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 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 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 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 pub fn client(mut self, client: Client) -> CrunchyrollBuilder {
798 self.client = client;
799 self
800 }
801
802 pub fn locale(mut self, locale: Locale) -> CrunchyrollBuilder {
805 self.locale = locale;
806 self
807 }
808
809 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 pub fn stream_platform(mut self, stream_platform: StreamPlatform) -> CrunchyrollBuilder {
829 self.stream_platform = stream_platform;
830 self
831 }
832
833 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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 #[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};