Spaces:
Paused
Paused
Upload main.rs
Browse files- src/main.rs +55 -8
src/main.rs
CHANGED
|
@@ -1212,6 +1212,21 @@ fn forwarded_proto(headers: &HeaderMap) -> String {
|
|
| 1212 |
.to_string()
|
| 1213 |
}
|
| 1214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
#[derive(Deserialize)]
|
| 1216 |
struct PairStatusQuery {
|
| 1217 |
code: String,
|
|
@@ -1337,12 +1352,17 @@ async fn pair_page(Host(host): Host, headers: HeaderMap, Query(q): Query<PairPag
|
|
| 1337 |
let code = q.code.unwrap_or_default().trim().to_string();
|
| 1338 |
let safe_code = htmlesc(&code);
|
| 1339 |
let scheme = forwarded_proto(&headers);
|
| 1340 |
-
let channel_url = format!("{scheme}://{host}/pair/channel");
|
| 1341 |
let app_id = deezer_app_id();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1342 |
|
| 1343 |
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1344 |
-
let
|
| 1345 |
-
let app_id_js = serde_json::to_string(&app_id).unwrap_or_else(|_| "\"\"".to_string());
|
| 1346 |
|
| 1347 |
let mut html = String::with_capacity(6000);
|
| 1348 |
html.push_str(r#"<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">"#);
|
|
@@ -1360,16 +1380,42 @@ async fn pair_page(Host(host): Host, headers: HeaderMap, Query(q): Query<PairPag
|
|
| 1360 |
html.push_str(r#"<script>(function(){"#);
|
| 1361 |
html.push_str("var code=");
|
| 1362 |
html.push_str(&code_js);
|
| 1363 |
-
html.push_str(r#";var
|
| 1364 |
-
html.push_str(&
|
| 1365 |
-
html.push_str(r#",
|
| 1366 |
-
html.push_str(&channel_url_js);
|
| 1367 |
-
html.push_str(r#"});return true;}catch(e){return false;}}if(!initDZ()){setMsg('err','Не удалось инициализировать Deezer SDK.');return;}document.getElementById('dz-login').addEventListener('click',function(){setMsg('','Ожидаю Deezer…');DZ.login(function(resp){if(resp&&resp.authResponse&&resp.authResponse.accessToken){setMsg('','Сохраняю токен…');postToken(resp.authResponse.accessToken,resp.authResponse.userID).then(function(r){if(r.ok&&!r.json.error){setMsg('ok','Готово ✓ Вернись на телевизор.');}else{setMsg('err','Ошибка сохранения: '+(r.json.error||'error'));}}).catch(function(){setMsg('err','Ошибка сети при сохранении');});}else{setMsg('err','Вход отменён или не удалось получить токен');}},{perms:'basic_access,email,manage_library,listening_history'});});}function loadSdk(){var s=document.createElement('script');s.src='https://cdns-files.deezer.com/js/min/dz.js';s.onload=function(){start();};s.onerror=function(){setMsg('err','Не удалось загрузить Deezer SDK (dz.js). Проверь сеть/блокировщик.');};document.head.appendChild(s);}loadSdk();})();</script>"#);
|
| 1368 |
html.push_str(r#"<p class="muted" style="margin:14px 0 0">Если Deezer пишет про неверный redirect/domain — нужно указать свой <b>DEEZER_APP_ID</b> для домена этого сервера.</p>"#);
|
| 1369 |
html.push_str(r#"</div></body></html>"#);
|
| 1370 |
Html(html)
|
| 1371 |
}
|
| 1372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1373 |
#[derive(Deserialize)]
|
| 1374 |
struct PairCompleteForm {
|
| 1375 |
code: String,
|
|
@@ -1957,6 +2003,7 @@ async fn main() {
|
|
| 1957 |
.route("/pair", get(pair_page))
|
| 1958 |
.route("/pair/channel", get(pair_channel))
|
| 1959 |
.route("/pair/oauth", post(pair_oauth))
|
|
|
|
| 1960 |
.route("/pair/complete", post(pair_complete))
|
| 1961 |
.route("/oauth/me", post(oauth_me))
|
| 1962 |
.route("/oauth/playlists", post(oauth_playlists))
|
|
|
|
| 1212 |
.to_string()
|
| 1213 |
}
|
| 1214 |
|
| 1215 |
+
fn urlenc(s: &str) -> String {
|
| 1216 |
+
let mut out = String::with_capacity(s.len() + 16);
|
| 1217 |
+
for &b in s.as_bytes() {
|
| 1218 |
+
let c = b as char;
|
| 1219 |
+
let ok = matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~');
|
| 1220 |
+
if ok {
|
| 1221 |
+
out.push(c);
|
| 1222 |
+
} else {
|
| 1223 |
+
out.push('%');
|
| 1224 |
+
out.push_str(&format!("{:02X}", b));
|
| 1225 |
+
}
|
| 1226 |
+
}
|
| 1227 |
+
out
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
#[derive(Deserialize)]
|
| 1231 |
struct PairStatusQuery {
|
| 1232 |
code: String,
|
|
|
|
| 1352 |
let code = q.code.unwrap_or_default().trim().to_string();
|
| 1353 |
let safe_code = htmlesc(&code);
|
| 1354 |
let scheme = forwarded_proto(&headers);
|
|
|
|
| 1355 |
let app_id = deezer_app_id();
|
| 1356 |
+
let redirect_url = format!("{scheme}://{host}/pair/oauth_cb?code={}", urlenc(&code));
|
| 1357 |
+
let oauth_url = format!(
|
| 1358 |
+
"https://connect.deezer.com/oauth/auth.php?app_id={}&redirect_uri={}&perms={}&response_type=token",
|
| 1359 |
+
urlenc(&app_id),
|
| 1360 |
+
urlenc(&redirect_url),
|
| 1361 |
+
urlenc("basic_access,email,manage_library,listening_history")
|
| 1362 |
+
);
|
| 1363 |
|
| 1364 |
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1365 |
+
let oauth_url_js = serde_json::to_string(&oauth_url).unwrap_or_else(|_| "\"\"".to_string());
|
|
|
|
| 1366 |
|
| 1367 |
let mut html = String::with_capacity(6000);
|
| 1368 |
html.push_str(r#"<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">"#);
|
|
|
|
| 1380 |
html.push_str(r#"<script>(function(){"#);
|
| 1381 |
html.push_str("var code=");
|
| 1382 |
html.push_str(&code_js);
|
| 1383 |
+
html.push_str(r#";var oauthUrl="#);
|
| 1384 |
+
html.push_str(&oauth_url_js);
|
| 1385 |
+
html.push_str(r#";var msg=document.getElementById('msg');function setMsg(cls,text){msg.className=cls?(cls+' muted'):'muted';msg.textContent=text;}if(!code){setMsg('err','Нет кода пары. Открой ссылку заново с QR.');return;}document.getElementById('dz-login').addEventListener('click',function(){setMsg('','Открываю Deezer…');if(!oauthUrl){setMsg('err','Не удалось собрать Deezer OAuth URL');return;}window.location.href=oauthUrl;});})();</script>"#);
|
|
|
|
|
|
|
| 1386 |
html.push_str(r#"<p class="muted" style="margin:14px 0 0">Если Deezer пишет про неверный redirect/domain — нужно указать свой <b>DEEZER_APP_ID</b> для домена этого сервера.</p>"#);
|
| 1387 |
html.push_str(r#"</div></body></html>"#);
|
| 1388 |
Html(html)
|
| 1389 |
}
|
| 1390 |
|
| 1391 |
+
#[derive(Deserialize)]
|
| 1392 |
+
struct PairOauthCbQuery {
|
| 1393 |
+
code: Option<String>,
|
| 1394 |
+
}
|
| 1395 |
+
|
| 1396 |
+
async fn pair_oauth_cb(Query(q): Query<PairOauthCbQuery>) -> impl IntoResponse {
|
| 1397 |
+
let code = q.code.unwrap_or_default().trim().to_string();
|
| 1398 |
+
let safe_code = htmlesc(&code);
|
| 1399 |
+
let code_js = serde_json::to_string(&code).unwrap_or_else(|_| "\"\"".to_string());
|
| 1400 |
+
|
| 1401 |
+
let mut html = String::with_capacity(3500);
|
| 1402 |
+
html.push_str(r#"<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">"#);
|
| 1403 |
+
html.push_str(r#"<title>Deezer Login</title>"#);
|
| 1404 |
+
html.push_str(r#"<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#0b0b0b;color:#fff;margin:0;padding:20px}.box{max-width:420px;margin:0 auto;background:#151515;padding:18px;border-radius:12px}.muted{color:#aaa;font-size:13px;line-height:1.35}.ok{color:#1db954}.err{color:#ff5a5a}</style>"#);
|
| 1405 |
+
html.push_str(r#"</head><body><div class="box">"#);
|
| 1406 |
+
html.push_str(r#"<h2 style="margin:0 0 6px">Deezer: вход для ТВ</h2>"#);
|
| 1407 |
+
html.push_str(r#"<div class="muted">Код пары: <b>"#);
|
| 1408 |
+
html.push_str(&safe_code);
|
| 1409 |
+
html.push_str(r#"</b></div>"#);
|
| 1410 |
+
html.push_str(r#"<div id="msg" class="muted" style="margin:12px 0 0">Получаю токен…</div>"#);
|
| 1411 |
+
html.push_str(r#"<script>(function(){"#);
|
| 1412 |
+
html.push_str("var code=");
|
| 1413 |
+
html.push_str(&code_js);
|
| 1414 |
+
html.push_str(r#";var msg=document.getElementById('msg');function setMsg(cls,text){msg.className=cls?(cls+' muted'):'muted';msg.textContent=text;}function parseHash(){var h=window.location.hash||'';if(h.charAt(0)==='#')h=h.slice(1);var out={};h.split('&').forEach(function(p){if(!p)return;var i=p.indexOf('=');var k=i>=0?p.slice(0,i):p;var v=i>=0?p.slice(i+1):'';try{k=decodeURIComponent(k);}catch(e){}try{v=decodeURIComponent(v);}catch(e){}out[k]=v;});return out;}if(!code){setMsg('err','Нет кода пары. Вернись на телевизор и обнови QR/ссылку.');return;}var h=parseHash();var at=h.access_token||'';if(!at){setMsg('err','Deezer не вернул access_token. Попробуй ещё раз.');return;}try{history.replaceState(null,'',window.location.pathname+window.location.search);}catch(e){}fetch('/pair/oauth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:code,access_token:at})}).then(function(r){return r.json().catch(function(){return {};}).then(function(j){return {ok:r.ok,json:j};});}).then(function(r){if(r.ok&&!r.json.error){setMsg('ok','Готово ✓ Вернись на телевизор.');}else{setMsg('err','Ошибка сохранения: '+(r.json.error||'error'));}}).catch(function(){setMsg('err','Ошибка сети при сохранении');});})();</script>"#);
|
| 1415 |
+
html.push_str(r#"</div></body></html>"#);
|
| 1416 |
+
(StatusCode::OK, Html(html)).into_response()
|
| 1417 |
+
}
|
| 1418 |
+
|
| 1419 |
#[derive(Deserialize)]
|
| 1420 |
struct PairCompleteForm {
|
| 1421 |
code: String,
|
|
|
|
| 2003 |
.route("/pair", get(pair_page))
|
| 2004 |
.route("/pair/channel", get(pair_channel))
|
| 2005 |
.route("/pair/oauth", post(pair_oauth))
|
| 2006 |
+
.route("/pair/oauth_cb", get(pair_oauth_cb))
|
| 2007 |
.route("/pair/complete", post(pair_complete))
|
| 2008 |
.route("/oauth/me", post(oauth_me))
|
| 2009 |
.route("/oauth/playlists", post(oauth_playlists))
|