Updated the ytmusic cookies situation
This commit is contained in:
@@ -242,3 +242,25 @@ pub async fn save_config(config: &AppConfig) -> Result<AppConfig, ApiError> {
|
||||
}
|
||||
resp.json().await.map_err(|e| ApiError(e.to_string()))
|
||||
}
|
||||
|
||||
// --- YouTube Auth ---
|
||||
|
||||
pub async fn get_ytauth_status() -> Result<YtAuthStatus, ApiError> {
|
||||
get_json(&format!("{BASE}/ytauth/status")).await
|
||||
}
|
||||
|
||||
pub async fn ytauth_login_start() -> Result<serde_json::Value, ApiError> {
|
||||
post_empty(&format!("{BASE}/ytauth/login-start")).await
|
||||
}
|
||||
|
||||
pub async fn ytauth_login_stop() -> Result<serde_json::Value, ApiError> {
|
||||
post_empty(&format!("{BASE}/ytauth/login-stop")).await
|
||||
}
|
||||
|
||||
pub async fn ytauth_refresh() -> Result<serde_json::Value, ApiError> {
|
||||
post_empty(&format!("{BASE}/ytauth/refresh")).await
|
||||
}
|
||||
|
||||
pub async fn ytauth_clear_cookies() -> Result<(), ApiError> {
|
||||
delete(&format!("{BASE}/ytauth/cookies")).await
|
||||
}
|
||||
|
||||
@@ -3,17 +3,20 @@ use web_sys::HtmlSelectElement;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::api;
|
||||
use crate::types::AppConfig;
|
||||
use crate::types::{AppConfig, YtAuthStatus};
|
||||
|
||||
#[function_component(SettingsPage)]
|
||||
pub fn settings_page() -> Html {
|
||||
let config = use_state(|| None::<AppConfig>);
|
||||
let error = use_state(|| None::<String>);
|
||||
let message = use_state(|| None::<String>);
|
||||
let ytauth = use_state(|| None::<YtAuthStatus>);
|
||||
let ytauth_loading = use_state(|| false);
|
||||
|
||||
{
|
||||
let config = config.clone();
|
||||
let error = error.clone();
|
||||
let ytauth = ytauth.clone();
|
||||
use_effect_with((), move |_| {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::get_config().await {
|
||||
@@ -21,6 +24,11 @@ pub fn settings_page() -> Html {
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
});
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
if let Ok(status) = api::get_ytauth_status().await {
|
||||
ytauth.set(Some(status));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,6 +76,195 @@ pub fn settings_page() -> Html {
|
||||
return html! { <p class="loading">{ "Loading configuration..." }</p> };
|
||||
};
|
||||
|
||||
// Build YouTube auth card HTML outside the main html! macro
|
||||
let ytauth_html = {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
if let Some(ref status) = *ytauth {
|
||||
if status.login_session_active {
|
||||
let vnc_url = status.vnc_url.clone().unwrap_or_default();
|
||||
let on_done = {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
ytauth_loading.set(true);
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::ytauth_login_stop().await {
|
||||
Ok(_) => {
|
||||
message.set(Some("YouTube login complete! Cookies exported.".into()));
|
||||
if let Ok(s) = api::get_ytauth_status().await {
|
||||
ytauth.set(Some(s));
|
||||
}
|
||||
}
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
ytauth_loading.set(false);
|
||||
});
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<p class="text-sm">{ "Log into YouTube in the browser below, then click Done." }</p>
|
||||
if !vnc_url.is_empty() {
|
||||
<p class="text-sm"><a href={vnc_url.clone()} target="_blank">{ "Open login window" }</a></p>
|
||||
<iframe src={vnc_url} style="width:100%;height:500px;border:1px solid var(--border);border-radius:var(--radius);margin:0.5rem 0;" />
|
||||
}
|
||||
<button class="btn btn-primary" onclick={on_done} disabled={*ytauth_loading}>
|
||||
{ if *ytauth_loading { "Finishing..." } else { "Done \u{2014} I've logged in" } }
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
} else if status.authenticated {
|
||||
let age_text = status.cookie_age_hours
|
||||
.map(|h| format!("cookies {h:.0}h old"))
|
||||
.unwrap_or_else(|| "authenticated".into());
|
||||
let on_refresh = {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
ytauth_loading.set(true);
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::ytauth_refresh().await {
|
||||
Ok(_) => {
|
||||
message.set(Some("Cookies refreshed".into()));
|
||||
if let Ok(s) = api::get_ytauth_status().await {
|
||||
ytauth.set(Some(s));
|
||||
}
|
||||
}
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
ytauth_loading.set(false);
|
||||
});
|
||||
})
|
||||
};
|
||||
let on_clear = {
|
||||
let ytauth = ytauth.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
let ytauth = ytauth.clone();
|
||||
let message = message.clone();
|
||||
let error = error.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::ytauth_clear_cookies().await {
|
||||
Ok(_) => {
|
||||
message.set(Some("YouTube auth cleared".into()));
|
||||
if let Ok(s) = api::get_ytauth_status().await {
|
||||
ytauth.set(Some(s));
|
||||
}
|
||||
}
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<p>
|
||||
<span class="badge badge-success">{ "Authenticated" }</span>
|
||||
<span class="text-muted text-sm" style="margin-left: 0.5rem;">{ age_text }</span>
|
||||
</p>
|
||||
if status.refresh_enabled {
|
||||
<p class="text-sm text-muted">{ "Auto-refresh is enabled" }</p>
|
||||
}
|
||||
<div style="display:flex;gap:0.5rem;margin-top:0.5rem;">
|
||||
<button class="btn btn-sm btn-secondary" onclick={on_refresh} disabled={*ytauth_loading}>
|
||||
{ if *ytauth_loading { "Refreshing..." } else { "Refresh Now" } }
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick={on_clear}>
|
||||
{ "Clear Authentication" }
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
let on_start = {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let error = error.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
let ytauth = ytauth.clone();
|
||||
let ytauth_loading = ytauth_loading.clone();
|
||||
let error = error.clone();
|
||||
ytauth_loading.set(true);
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match api::ytauth_login_start().await {
|
||||
Ok(_) => {
|
||||
if let Ok(s) = api::get_ytauth_status().await {
|
||||
ytauth.set(Some(s));
|
||||
}
|
||||
}
|
||||
Err(e) => error.set(Some(e.0)),
|
||||
}
|
||||
ytauth_loading.set(false);
|
||||
});
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<p class="text-muted text-sm">
|
||||
{ "Authenticate with YouTube for higher download rate limits (~2000/hr vs ~300/hr) and access to age-restricted content. " }
|
||||
{ "This launches a browser where you log into your Google account." }
|
||||
</p>
|
||||
<div class="card" style="border-color: var(--warning); background: rgba(234, 179, 8, 0.08); margin: 0.75rem 0;">
|
||||
<p class="text-sm" style="margin:0;">
|
||||
<strong style="color: var(--warning);">{ "Warning: " }</strong>
|
||||
{ "YouTube may permanently suspend accounts that are used with third-party download tools. " }
|
||||
<strong>{ "Use a throwaway Google account" }</strong>
|
||||
{ " \u{2014} do not log in with your primary account." }
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick={on_start} disabled={*ytauth_loading}>
|
||||
{ if *ytauth_loading { "Starting..." } else { "Authenticate YouTube" } }
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! { <p class="text-muted text-sm">{ "Loading..." }</p> }
|
||||
}
|
||||
};
|
||||
|
||||
let ytdlp_version_html = if let Some(ref status) = *ytauth {
|
||||
let version = status.ytdlp_version.clone().unwrap_or_else(|| "not found".into());
|
||||
if status.ytdlp_update_available {
|
||||
let latest = status.ytdlp_latest.clone().unwrap_or_default();
|
||||
html! {
|
||||
<div class="card" style="border-color: var(--warning); background: rgba(234, 179, 8, 0.08); margin: 0 0 0.75rem 0; padding: 0.5rem 0.75rem;">
|
||||
<p class="text-sm" style="margin:0;">
|
||||
<strong style="color: var(--warning);">{ "yt-dlp update available: " }</strong>
|
||||
{ format!("{version} \u{2192} {latest}") }
|
||||
<span class="text-muted">{ " \u{2014} run " }</span>
|
||||
<code>{ "pip install -U yt-dlp" }</code>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<p class="text-muted text-sm" style="margin: 0 0 0.75rem 0;">
|
||||
{ format!("yt-dlp {version}") }
|
||||
<span style="color: var(--success); margin-left: 0.3rem;">{ "\u{2713} up to date" }</span>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<div class="page-header">
|
||||
@@ -211,18 +408,6 @@ pub fn settings_page() -> Html {
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{ "Cookies Path (optional)" }</label>
|
||||
<input type="text" value={c.download.cookies_path.clone().unwrap_or_default()}
|
||||
placeholder="~/.config/shanty/cookies.txt"
|
||||
oninput={let config = config.clone(); Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
let mut cfg = (*config).clone().unwrap();
|
||||
let v = input.value();
|
||||
cfg.download.cookies_path = if v.is_empty() { None } else { Some(v) };
|
||||
config.set(Some(cfg));
|
||||
})} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{ "Rate Limit (requests/hour, guest)" }</label>
|
||||
<input type="number" value={c.download.rate_limit.to_string()}
|
||||
@@ -249,6 +434,13 @@ pub fn settings_page() -> Html {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// YouTube Authentication
|
||||
<div class="card">
|
||||
<h3>{ "YouTube Authentication" }</h3>
|
||||
{ ytdlp_version_html }
|
||||
{ ytauth_html }
|
||||
</div>
|
||||
|
||||
// Indexing
|
||||
<div class="card">
|
||||
<h3>{ "Indexing" }</h3>
|
||||
|
||||
@@ -160,6 +160,24 @@ pub struct TrackResult {
|
||||
pub score: u8,
|
||||
}
|
||||
|
||||
// --- YouTube Auth ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct YtAuthStatus {
|
||||
pub authenticated: bool,
|
||||
pub cookie_age_hours: Option<f64>,
|
||||
pub cookie_count: Option<i64>,
|
||||
pub refresh_enabled: bool,
|
||||
pub login_session_active: bool,
|
||||
pub vnc_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ytdlp_version: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ytdlp_latest: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ytdlp_update_available: bool,
|
||||
}
|
||||
|
||||
// --- Downloads ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
@@ -303,6 +321,19 @@ pub struct DownloadConfigFe {
|
||||
pub rate_limit: u32,
|
||||
#[serde(default)]
|
||||
pub rate_limit_auth: u32,
|
||||
#[serde(default)]
|
||||
pub cookie_refresh_enabled: bool,
|
||||
#[serde(default = "default_cookie_refresh_hours")]
|
||||
pub cookie_refresh_hours: u32,
|
||||
#[serde(default = "default_vnc_port")]
|
||||
pub vnc_port: u16,
|
||||
}
|
||||
|
||||
fn default_cookie_refresh_hours() -> u32 {
|
||||
6
|
||||
}
|
||||
fn default_vnc_port() -> u16 {
|
||||
6080
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
|
||||
Reference in New Issue
Block a user