recycleactor commited on
Commit
6e726e5
·
verified ·
1 Parent(s): c2658b8

Upload 2 files

Browse files
Files changed (1) hide show
  1. src/main.rs +331 -10
src/main.rs CHANGED
@@ -1,14 +1,15 @@
1
  use axum::{
2
  routing::{get, post},
3
  http::StatusCode,
4
- response::IntoResponse,
5
- extract::{Json, State, Query},
6
  Router,
7
  };
8
  use tower_http::{cors::{CorsLayer, Any}, compression::CompressionLayer, trace::TraceLayer};
9
  use http::{Method, header::CONTENT_TYPE};
10
  use serde_json::json;
11
  use serde::Deserialize;
 
12
  use std::sync::{Arc, RwLock};
13
  use bytes::Bytes;
14
  use futures_util::StreamExt;
@@ -27,6 +28,12 @@ type BoxErr = Box<dyn std::error::Error + Send + Sync + 'static>;
27
  mod api;
28
  use api::{APIClient, APIError, Format};
29
 
 
 
 
 
 
 
30
  fn json_error(status: StatusCode, message: impl Into<String>) -> axum::response::Response {
31
  (status, Json(json!({ "error": message.into() }))).into_response()
32
  }
@@ -53,7 +60,7 @@ struct RequestParams {
53
  arl: Option<String>,
54
  }
55
 
56
- async fn get_url(State(state): State<Arc<RwLock<APIClient>>>, Json(req): Json<RequestParams>) -> impl IntoResponse {
57
  if req.formats.is_empty() {
58
  return (StatusCode::BAD_REQUEST, "Format list cannot be empty".to_string());
59
  }
@@ -64,7 +71,7 @@ async fn get_url(State(state): State<Arc<RwLock<APIClient>>>, Json(req): Json<Re
64
  let mut client = if let Some(arl) = req.arl.clone().filter(|s| !s.trim().is_empty()) {
65
  APIClient::new_with_arl(arl)
66
  } else {
67
- state.read().unwrap().clone()
68
  };
69
  let old_license = client.license_token.clone();
70
 
@@ -89,7 +96,7 @@ async fn get_url(State(state): State<Arc<RwLock<APIClient>>>, Json(req): Json<Re
89
  };
90
 
91
  if req.arl.is_none() && client.license_token != old_license {
92
- let mut client_write = state.write().unwrap();
93
  *client_write = client;
94
  }
95
 
@@ -167,7 +174,7 @@ struct StreamParams {
167
  arl: Option<String>,
168
  }
169
 
170
- async fn stream(State(state): State<Arc<RwLock<APIClient>>>, Query(q): Query<StreamParams>) -> impl IntoResponse {
171
  let formats = match q.format.as_deref().unwrap_or("AUTO") {
172
  "FLAC" => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
173
  "MP3_320" => vec![Format::MP3_320, Format::MP3_128],
@@ -179,7 +186,7 @@ async fn stream(State(state): State<Arc<RwLock<APIClient>>>, Query(q): Query<Str
179
  let mut client = if let Some(arl) = q.arl.clone().filter(|s| !s.trim().is_empty()) {
180
  APIClient::new_with_arl(arl)
181
  } else {
182
- state.read().unwrap().clone()
183
  };
184
 
185
  let resp: Result<DeezerTrackList, APIError> = client
@@ -545,7 +552,9 @@ async fn login(Json(req): Json<LoginReq>) -> impl IntoResponse {
545
  .build()
546
  .unwrap();
547
 
 
548
  if let Ok(access_token) = oauth_access_token(&client, &email, &pass).await {
 
549
  let _ = client
550
  .get("https://api.deezer.com/platform/generic/track/80085")
551
  .header("Authorization", format!("Bearer {}", access_token))
@@ -617,7 +626,309 @@ async fn login(Json(req): Json<LoginReq>) -> impl IntoResponse {
617
  return json_error(StatusCode::BAD_GATEWAY, "no arl");
618
  }
619
 
620
- (StatusCode::OK, Json(json!({ "arl": arl, "token": token, "uid": uid }))).into_response()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  }
622
 
623
  #[tokio::main]
@@ -626,7 +937,10 @@ async fn main() {
626
  let port = std::env::var("PORT").unwrap_or("8000".to_string());
627
  let port: u16 = port.parse().unwrap_or(8000);
628
 
629
- let shared_state = Arc::new(RwLock::new(APIClient::new()));
 
 
 
630
 
631
  let cors = CorsLayer::new()
632
  .allow_origin(Any)
@@ -644,7 +958,14 @@ async fn main() {
644
  .route("/playlists", post(playlists))
645
  .route("/playlist", post(playlist_tracks))
646
  .route("/login", post(login))
647
- .with_state(shared_state)
 
 
 
 
 
 
 
648
  .layer(cors)
649
  .layer(CompressionLayer::new())
650
  .layer(TraceLayer::new_for_http());
 
1
  use axum::{
2
  routing::{get, post},
3
  http::StatusCode,
4
+ response::{IntoResponse, Html},
5
+ extract::{Json, State, Query, Form},
6
  Router,
7
  };
8
  use tower_http::{cors::{CorsLayer, Any}, compression::CompressionLayer, trace::TraceLayer};
9
  use http::{Method, header::CONTENT_TYPE};
10
  use serde_json::json;
11
  use serde::Deserialize;
12
+ use std::collections::HashMap;
13
  use std::sync::{Arc, RwLock};
14
  use bytes::Bytes;
15
  use futures_util::StreamExt;
 
28
  mod api;
29
  use api::{APIClient, APIError, Format};
30
 
31
+ #[derive(Clone)]
32
+ struct AppState {
33
+ api: Arc<RwLock<APIClient>>,
34
+ pair: Arc<RwLock<HashMap<String, PairSession>>>,
35
+ }
36
+
37
  fn json_error(status: StatusCode, message: impl Into<String>) -> axum::response::Response {
38
  (status, Json(json!({ "error": message.into() }))).into_response()
39
  }
 
60
  arl: Option<String>,
61
  }
62
 
63
+ async fn get_url(State(state): State<AppState>, Json(req): Json<RequestParams>) -> impl IntoResponse {
64
  if req.formats.is_empty() {
65
  return (StatusCode::BAD_REQUEST, "Format list cannot be empty".to_string());
66
  }
 
71
  let mut client = if let Some(arl) = req.arl.clone().filter(|s| !s.trim().is_empty()) {
72
  APIClient::new_with_arl(arl)
73
  } else {
74
+ state.api.read().unwrap().clone()
75
  };
76
  let old_license = client.license_token.clone();
77
 
 
96
  };
97
 
98
  if req.arl.is_none() && client.license_token != old_license {
99
+ let mut client_write = state.api.write().unwrap();
100
  *client_write = client;
101
  }
102
 
 
174
  arl: Option<String>,
175
  }
176
 
177
+ async fn stream(State(state): State<AppState>, Query(q): Query<StreamParams>) -> impl IntoResponse {
178
  let formats = match q.format.as_deref().unwrap_or("AUTO") {
179
  "FLAC" => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
180
  "MP3_320" => vec![Format::MP3_320, Format::MP3_128],
 
186
  let mut client = if let Some(arl) = q.arl.clone().filter(|s| !s.trim().is_empty()) {
187
  APIClient::new_with_arl(arl)
188
  } else {
189
+ state.api.read().unwrap().clone()
190
  };
191
 
192
  let resp: Result<DeezerTrackList, APIError> = client
 
552
  .build()
553
  .unwrap();
554
 
555
+ let mut access_token_out: Option<String> = None;
556
  if let Ok(access_token) = oauth_access_token(&client, &email, &pass).await {
557
+ access_token_out = Some(access_token.clone());
558
  let _ = client
559
  .get("https://api.deezer.com/platform/generic/track/80085")
560
  .header("Authorization", format!("Bearer {}", access_token))
 
626
  return json_error(StatusCode::BAD_GATEWAY, "no arl");
627
  }
628
 
629
+ (StatusCode::OK, Json(json!({ "arl": arl, "token": token, "uid": uid, "access_token": access_token_out }))).into_response()
630
+ }
631
+
632
+ #[derive(Clone)]
633
+ struct PairSession {
634
+ created_ms: u128,
635
+ access_token: Option<String>,
636
+ user_id: Option<String>,
637
+ error: Option<String>,
638
+ }
639
+
640
+ fn gen_pair_code() -> String {
641
+ let ms = SystemTime::now()
642
+ .duration_since(UNIX_EPOCH)
643
+ .map(|d| d.as_millis())
644
+ .unwrap_or(0);
645
+ let hex = format!("{:x}", md5::compute(format!("pair:{ms}:{SECRET}").as_bytes()));
646
+ hex.chars().take(10).collect::<String>()
647
+ }
648
+
649
+ #[derive(Deserialize)]
650
+ struct PairStatusQuery {
651
+ code: String,
652
+ }
653
+
654
+ async fn pair_start(State(state): State<AppState>) -> impl IntoResponse {
655
+ let code = gen_pair_code();
656
+ let created_ms = SystemTime::now()
657
+ .duration_since(UNIX_EPOCH)
658
+ .map(|d| d.as_millis())
659
+ .unwrap_or(0);
660
+ {
661
+ let mut map = state.pair.write().unwrap();
662
+ map.insert(
663
+ code.clone(),
664
+ PairSession {
665
+ created_ms,
666
+ access_token: None,
667
+ user_id: None,
668
+ error: None,
669
+ },
670
+ );
671
+ }
672
+ (StatusCode::OK, Json(json!({ "code": code }))).into_response()
673
+ }
674
+
675
+ async fn pair_status(State(state): State<AppState>, Query(q): Query<PairStatusQuery>) -> impl IntoResponse {
676
+ let code = q.code.trim().to_string();
677
+ if code.is_empty() {
678
+ return json_error(StatusCode::BAD_REQUEST, "code required");
679
+ }
680
+
681
+ let map = state.pair.read().unwrap();
682
+ let Some(sess) = map.get(&code) else {
683
+ return json_error(StatusCode::NOT_FOUND, "no such code");
684
+ };
685
+
686
+ if let Some(err) = &sess.error {
687
+ return (StatusCode::OK, Json(json!({ "status": "error", "error": err }))).into_response();
688
+ }
689
+ if let Some(tok) = &sess.access_token {
690
+ return (
691
+ StatusCode::OK,
692
+ Json(json!({ "status": "ok", "access_token": tok, "user_id": sess.user_id })),
693
+ )
694
+ .into_response();
695
+ }
696
+
697
+ (StatusCode::OK, Json(json!({ "status": "pending" }))).into_response()
698
+ }
699
+
700
+ #[derive(Deserialize)]
701
+ struct PairPageQuery {
702
+ code: Option<String>,
703
+ }
704
+
705
+ async fn pair_page(Query(q): Query<PairPageQuery>) -> impl IntoResponse {
706
+ let code = q.code.unwrap_or_default();
707
+ let safe_code = htmlesc(&code);
708
+ let html = format!(
709
+ "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
710
+ <title>Deezer Login</title>\
711
+ <style>body{{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#0b0b0b;color:#fff;margin:0;padding:20px}}\
712
+ .box{{max-width:420px;margin:0 auto;background:#151515;padding:18px;border-radius:12px}}\
713
+ input{{width:100%;padding:12px 14px;margin:10px 0;border-radius:10px;border:1px solid #333;background:#0f0f0f;color:#fff;font-size:16px}}\
714
+ button{{width:100%;padding:12px 14px;border-radius:10px;border:0;background:#1db954;color:#000;font-weight:700;font-size:16px}}\
715
+ .muted{{color:#aaa;font-size:13px;line-height:1.35}}</style>\
716
+ </head><body><div class=\"box\">\
717
+ <h2 style=\"margin:0 0 6px\">Вход Deezer</h2>\
718
+ <div class=\"muted\">Код: <b>{safe_code}</b></div>\
719
+ <form method=\"POST\" action=\"/pair/complete\">\
720
+ <input type=\"hidden\" name=\"code\" value=\"{safe_code}\">\
721
+ <input name=\"email\" type=\"email\" placeholder=\"Email\" autocomplete=\"username\" required>\
722
+ <input name=\"password\" type=\"password\" placeholder=\"Пароль\" autocomplete=\"current-password\" required>\
723
+ <button type=\"submit\">Войти</button>\
724
+ </form>\
725
+ <p class=\"muted\" style=\"margin:12px 0 0\">Пароль используется только для получения токена доступа и не сохраняется.</p>\
726
+ </div></body></html>"
727
+ );
728
+ Html(html)
729
+ }
730
+
731
+ #[derive(Deserialize)]
732
+ struct PairCompleteForm {
733
+ code: String,
734
+ email: String,
735
+ password: String,
736
+ }
737
+
738
+ fn htmlesc(s: &str) -> String {
739
+ s.replace('&', "&amp;")
740
+ .replace('<', "&lt;")
741
+ .replace('>', "&gt;")
742
+ .replace('"', "&quot;")
743
+ .replace('\'', "&#39;")
744
+ }
745
+
746
+ async fn api_deezer_get(
747
+ client: &reqwest::Client,
748
+ path: &str,
749
+ access_token: &str,
750
+ extra: &[(&str, String)],
751
+ ) -> Result<serde_json::Value, String> {
752
+ let url = format!("https://api.deezer.com{path}");
753
+ let mut req = client
754
+ .get(url)
755
+ .header("Authorization", format!("Bearer {}", access_token))
756
+ .header(ACCEPT, "*/*")
757
+ .header("Accept-Language", "en-US,en;q=0.9")
758
+ .header(
759
+ "User-Agent",
760
+ "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
761
+ )
762
+ .query(&[("access_token", access_token)]);
763
+ if !extra.is_empty() {
764
+ req = req.query(extra);
765
+ }
766
+ let r = req.send().await.map_err(|_| "api:network".to_string())?;
767
+ let status = r.status();
768
+ let text = r.text().await.map_err(|_| "api:read".to_string())?;
769
+ let v: serde_json::Value = serde_json::from_str(&text).map_err(|_| {
770
+ let snip: String = text.chars().take(200).collect();
771
+ format!("api:parse:{}:{}", status.as_u16(), snip)
772
+ })?;
773
+ if let Some(e) = v.get("error") {
774
+ if e.is_object() {
775
+ let code = e.get("code").and_then(|x| x.as_i64()).unwrap_or(0);
776
+ let msg = e.get("message").and_then(|x| x.as_str()).unwrap_or("error");
777
+ return Err(format!("api:{code}:{msg}"));
778
+ }
779
+ return Err("api:error".to_string());
780
+ }
781
+ Ok(v)
782
+ }
783
+
784
+ async fn pair_complete(State(state): State<AppState>, Form(f): Form<PairCompleteForm>) -> impl IntoResponse {
785
+ let code = f.code.trim().to_string();
786
+ let email = f.email.trim().to_string();
787
+ let password = f.password;
788
+ if code.is_empty() || email.is_empty() || password.is_empty() {
789
+ return Html("<h3>Нужны code/email/password</h3>".to_string()).into_response();
790
+ }
791
+
792
+ {
793
+ let map = state.pair.read().unwrap();
794
+ if !map.contains_key(&code) {
795
+ return Html("<h3>Код не найден или истёк</h3>".to_string()).into_response();
796
+ }
797
+ }
798
+
799
+ let jar = Arc::new(Jar::default());
800
+ let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
801
+ jar.add_cookie_str("comeback=1; Domain=.deezer.com", &url_deezer);
802
+ let client = reqwest::Client::builder()
803
+ .cookie_provider(jar)
804
+ .timeout(std::time::Duration::from_secs(20))
805
+ .build()
806
+ .unwrap();
807
+
808
+ let pass_md5 = format!("{:x}", md5::compute(password.as_bytes()));
809
+ let access_token = match oauth_access_token(&client, &email, &pass_md5).await {
810
+ Ok(t) => t,
811
+ Err(e) => {
812
+ let mut map = state.pair.write().unwrap();
813
+ if let Some(s) = map.get_mut(&code) {
814
+ s.error = Some(e.clone());
815
+ }
816
+ return Html(format!("<h3>Ошибка входа</h3><pre>{}</pre>", htmlesc(&e))).into_response();
817
+ }
818
+ };
819
+
820
+ let _ = client
821
+ .get("https://api.deezer.com/platform/generic/track/80085")
822
+ .header("Authorization", format!("Bearer {}", access_token))
823
+ .send()
824
+ .await;
825
+
826
+ let me = match api_deezer_get(&client, "/user/me", &access_token, &[]).await {
827
+ Ok(v) => v,
828
+ Err(e) => {
829
+ let mut map = state.pair.write().unwrap();
830
+ if let Some(s) = map.get_mut(&code) {
831
+ s.error = Some(e.clone());
832
+ }
833
+ return Html(format!("<h3>Ошибка Deezer API</h3><pre>{}</pre>", htmlesc(&e))).into_response();
834
+ }
835
+ };
836
+ let uid = me.get("id").and_then(|x| x.as_i64()).map(|n| n.to_string());
837
+
838
+ {
839
+ let mut map = state.pair.write().unwrap();
840
+ if let Some(s) = map.get_mut(&code) {
841
+ s.access_token = Some(access_token);
842
+ s.user_id = uid;
843
+ s.error = None;
844
+ }
845
+ }
846
+
847
+ Html("<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>".to_string()).into_response()
848
+ }
849
+
850
+ #[derive(Debug, Deserialize)]
851
+ struct OauthMeReq {
852
+ access_token: String,
853
+ }
854
+
855
+ async fn oauth_me(Json(req): Json<OauthMeReq>) -> impl IntoResponse {
856
+ let token = req.access_token.trim().to_string();
857
+ if token.is_empty() {
858
+ return json_error(StatusCode::BAD_REQUEST, "access_token required");
859
+ }
860
+ let client = reqwest::Client::builder()
861
+ .timeout(std::time::Duration::from_secs(15))
862
+ .build()
863
+ .unwrap();
864
+ match api_deezer_get(&client, "/user/me", &token, &[]).await {
865
+ Ok(v) => (StatusCode::OK, Json(v)).into_response(),
866
+ Err(e) => json_error(StatusCode::BAD_GATEWAY, e),
867
+ }
868
+ }
869
+
870
+ #[derive(Debug, Deserialize)]
871
+ struct OauthPlaylistsReq {
872
+ access_token: String,
873
+ index: Option<u32>,
874
+ limit: Option<u32>,
875
+ }
876
+
877
+ async fn oauth_playlists(Json(req): Json<OauthPlaylistsReq>) -> impl IntoResponse {
878
+ let token = req.access_token.trim().to_string();
879
+ if token.is_empty() {
880
+ return json_error(StatusCode::BAD_REQUEST, "access_token required");
881
+ }
882
+ let index = req.index.unwrap_or(0).to_string();
883
+ let limit = req.limit.unwrap_or(50).to_string();
884
+ let client = reqwest::Client::builder()
885
+ .timeout(std::time::Duration::from_secs(15))
886
+ .build()
887
+ .unwrap();
888
+ let me = match api_deezer_get(&client, "/user/me", &token, &[]).await {
889
+ Ok(v) => v,
890
+ Err(e) => return json_error(StatusCode::BAD_GATEWAY, e),
891
+ };
892
+ let uid = me.get("id").and_then(|x| x.as_i64()).unwrap_or(0);
893
+ if uid <= 0 {
894
+ return json_error(StatusCode::BAD_GATEWAY, "api:no_user_id");
895
+ }
896
+ let path = format!("/user/{uid}/playlists");
897
+ let extra = [("index", index), ("limit", limit)];
898
+ match api_deezer_get(&client, &path, &token, &extra).await {
899
+ Ok(v) => (StatusCode::OK, Json(v)).into_response(),
900
+ Err(e) => json_error(StatusCode::BAD_GATEWAY, e),
901
+ }
902
+ }
903
+
904
+ #[derive(Debug, Deserialize)]
905
+ struct OauthPlaylistTracksReq {
906
+ access_token: String,
907
+ playlist_id: u64,
908
+ index: Option<u32>,
909
+ limit: Option<u32>,
910
+ }
911
+
912
+ async fn oauth_playlist_tracks(Json(req): Json<OauthPlaylistTracksReq>) -> impl IntoResponse {
913
+ let token = req.access_token.trim().to_string();
914
+ if token.is_empty() {
915
+ return json_error(StatusCode::BAD_REQUEST, "access_token required");
916
+ }
917
+ if req.playlist_id == 0 {
918
+ return json_error(StatusCode::BAD_REQUEST, "playlist_id required");
919
+ }
920
+ let index = req.index.unwrap_or(0).to_string();
921
+ let limit = req.limit.unwrap_or(100).to_string();
922
+ let client = reqwest::Client::builder()
923
+ .timeout(std::time::Duration::from_secs(15))
924
+ .build()
925
+ .unwrap();
926
+ let path = format!("/playlist/{}/tracks", req.playlist_id);
927
+ let extra = [("index", index), ("limit", limit)];
928
+ match api_deezer_get(&client, &path, &token, &extra).await {
929
+ Ok(v) => (StatusCode::OK, Json(v)).into_response(),
930
+ Err(e) => json_error(StatusCode::BAD_GATEWAY, e),
931
+ }
932
  }
933
 
934
  #[tokio::main]
 
937
  let port = std::env::var("PORT").unwrap_or("8000".to_string());
938
  let port: u16 = port.parse().unwrap_or(8000);
939
 
940
+ let state = AppState {
941
+ api: Arc::new(RwLock::new(APIClient::new())),
942
+ pair: Arc::new(RwLock::new(HashMap::new())),
943
+ };
944
 
945
  let cors = CorsLayer::new()
946
  .allow_origin(Any)
 
958
  .route("/playlists", post(playlists))
959
  .route("/playlist", post(playlist_tracks))
960
  .route("/login", post(login))
961
+ .route("/pair/start", get(pair_start))
962
+ .route("/pair/status", get(pair_status))
963
+ .route("/pair", get(pair_page))
964
+ .route("/pair/complete", post(pair_complete))
965
+ .route("/oauth/me", post(oauth_me))
966
+ .route("/oauth/playlists", post(oauth_playlists))
967
+ .route("/oauth/playlist_tracks", post(oauth_playlist_tracks))
968
+ .with_state(state)
969
  .layer(cors)
970
  .layer(CompressionLayer::new())
971
  .layer(TraceLayer::new_for_http());