mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
refactor(unlock): restructure media unlock checker (#5044)
- Split the monolithic unlock checker into a module tree (mod.rs:9–133), wiring service-specific tasks while keeping exported Tauri commands untouched. - Centralize shared data and helpers in types.rs (1–40) and utils.rs (1–21) for reusable timestamp and emoji logic. - Move each provider’s logic into its own file (bilibili.rs, disney_plus.rs, netflix.rs, etc.), preserving behavior and making future additions or fixes localized.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
124
src-tauri/src/cmd/media_unlock_checker/bahamut.rs
Normal file
124
src-tauri/src/cmd/media_unlock_checker/bahamut.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use reqwest::{Client, cookie::Jar};
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||||
|
let cookie_store = Arc::new(Jar::default());
|
||||||
|
|
||||||
|
let client_with_cookies = match Client::builder()
|
||||||
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||||
|
.cookie_provider(Arc::clone(&cookie_store))
|
||||||
|
.build() {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to create client with cookies for Bahamut Anime: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
client.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_url = "https://ani.gamer.com.tw/ajax/getdeviceid.php";
|
||||||
|
let device_id = match client_with_cookies.get(device_url).send().await {
|
||||||
|
Ok(response) => match response.text().await {
|
||||||
|
Ok(text) => match Regex::new(r#""deviceid"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(re) => re
|
||||||
|
.captures(&text)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile deviceid regex for Bahamut Anime: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => String::new(),
|
||||||
|
},
|
||||||
|
Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if device_id.is_empty() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Bahamut Anime".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let url =
|
||||||
|
format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
|
||||||
|
|
||||||
|
let token_result = match client_with_cookies.get(&url).send().await {
|
||||||
|
Ok(response) => match response.text().await {
|
||||||
|
Ok(body) => {
|
||||||
|
if body.contains("animeSn") {
|
||||||
|
Some(body)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if token_result.is_none() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Bahamut Anime".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let region = match client_with_cookies
|
||||||
|
.get("https://ani.gamer.com.tw/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => match response.text().await {
|
||||||
|
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
|
||||||
|
Ok(region_re) => region_re
|
||||||
|
.captures(&body)
|
||||||
|
.and_then(|caps| caps.get(1))
|
||||||
|
.map(|m| {
|
||||||
|
let country_code = m.as_str();
|
||||||
|
let emoji = country_code_to_emoji(country_code);
|
||||||
|
format!("{emoji}{country_code}")
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile region regex for Bahamut Anime: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "Bahamut Anime".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src-tauri/src/cmd/media_unlock_checker/bilibili.rs
Normal file
91
src-tauri/src/cmd/media_unlock_checker/bilibili.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use reqwest::Client;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::get_local_date_string;
|
||||||
|
|
||||||
|
pub(super) async fn check_bilibili_china_mainland(client: &Client) -> UnlockItem {
|
||||||
|
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=82846771&qn=0&type=&otype=json&ep_id=307247&fourk=1&fnver=0&fnval=16&module=bangumi";
|
||||||
|
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(response) => match response.json::<Value>().await {
|
||||||
|
Ok(body) => {
|
||||||
|
let status = body
|
||||||
|
.get("code")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.map(|code| {
|
||||||
|
if code == 0 {
|
||||||
|
"Yes"
|
||||||
|
} else if code == -10403 {
|
||||||
|
"No"
|
||||||
|
} else {
|
||||||
|
"Failed"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or("Failed");
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "哔哩哔哩大陆".to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "哔哩哔哩大陆".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "哔哩哔哩大陆".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn check_bilibili_hk_mc_tw(client: &Client) -> UnlockItem {
|
||||||
|
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=18281381&cid=29892777&qn=0&type=&otype=json&ep_id=183799&fourk=1&fnver=0&fnval=16&module=bangumi";
|
||||||
|
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(response) => match response.json::<Value>().await {
|
||||||
|
Ok(body) => {
|
||||||
|
let status = body
|
||||||
|
.get("code")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.map(|code| {
|
||||||
|
if code == 0 {
|
||||||
|
"Yes"
|
||||||
|
} else if code == -10403 {
|
||||||
|
"No"
|
||||||
|
} else {
|
||||||
|
"Failed"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or("Failed");
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "哔哩哔哩港澳台".to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "哔哩哔哩港澳台".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "哔哩哔哩港澳台".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src-tauri/src/cmd/media_unlock_checker/chatgpt.rs
Normal file
94
src-tauri/src/cmd/media_unlock_checker/chatgpt.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
let url_country = "https://chat.openai.com/cdn-cgi/trace";
|
||||||
|
let result_country = client.get(url_country).send().await;
|
||||||
|
|
||||||
|
let region = match result_country {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Ok(body) = response.text().await {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for line in body.lines() {
|
||||||
|
if let Some(index) = line.find('=') {
|
||||||
|
let key = &line[..index];
|
||||||
|
let value = &line[index + 1..];
|
||||||
|
map.insert(key.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.get("loc").map(|loc| {
|
||||||
|
let emoji = country_code_to_emoji(loc);
|
||||||
|
format!("{emoji}{loc}")
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_ios = "https://ios.chat.openai.com/";
|
||||||
|
let result_ios = client.get(url_ios).send().await;
|
||||||
|
|
||||||
|
let ios_status = match result_ios {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Ok(body) = response.text().await {
|
||||||
|
let body_lower = body.to_lowercase();
|
||||||
|
if body_lower.contains("you may be connected to a disallowed isp") {
|
||||||
|
"Disallowed ISP"
|
||||||
|
} else if body_lower.contains("request is not allowed. please try again later.") {
|
||||||
|
"Yes"
|
||||||
|
} else if body_lower.contains("sorry, you have been blocked") {
|
||||||
|
"Blocked"
|
||||||
|
} else {
|
||||||
|
"Failed"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"Failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "Failed",
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_web = "https://api.openai.com/compliance/cookie_requirements";
|
||||||
|
let result_web = client.get(url_web).send().await;
|
||||||
|
|
||||||
|
let web_status = match result_web {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Ok(body) = response.text().await {
|
||||||
|
let body_lower = body.to_lowercase();
|
||||||
|
if body_lower.contains("unsupported_country") {
|
||||||
|
"Unsupported Country/Region"
|
||||||
|
} else {
|
||||||
|
"Yes"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"Failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "Failed",
|
||||||
|
};
|
||||||
|
|
||||||
|
results.push(UnlockItem {
|
||||||
|
name: "ChatGPT iOS".to_string(),
|
||||||
|
status: ios_status.to_string(),
|
||||||
|
region: region.clone(),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push(UnlockItem {
|
||||||
|
name: "ChatGPT Web".to_string(),
|
||||||
|
status: web_status.to_string(),
|
||||||
|
region,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
489
src-tauri/src/cmd/media_unlock_checker/disney_plus.rs
Normal file
489
src-tauri/src/cmd/media_unlock_checker/disney_plus.rs
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||||
|
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
|
||||||
|
let auth_header =
|
||||||
|
"Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
|
||||||
|
|
||||||
|
let device_req_body = serde_json::json!({
|
||||||
|
"deviceFamily": "browser",
|
||||||
|
"applicationRuntime": "chrome",
|
||||||
|
"deviceProfile": "windows",
|
||||||
|
"attributes": {}
|
||||||
|
});
|
||||||
|
|
||||||
|
let device_result = client
|
||||||
|
.post(device_api_url)
|
||||||
|
.header("authorization", auth_header)
|
||||||
|
.header("content-type", "application/json; charset=UTF-8")
|
||||||
|
.json(&device_req_body)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if device_result.is_err() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_response = match device_result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Disney+ device response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if device_response.status().as_u16() == 403 {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "No (IP Banned By Disney+)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_body = match device_response.text().await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(_) => {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Error: Cannot read response)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let re = match Regex::new(r#""assertion"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile assertion regex for Disney+: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let assertion = match re.captures(&device_body) {
|
||||||
|
Some(caps) => caps.get(1).map(|m| m.as_str().to_string()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if assertion.is_none() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Error: Cannot extract assertion)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
||||||
|
let assertion_str = match assertion {
|
||||||
|
Some(assertion) => assertion,
|
||||||
|
None => {
|
||||||
|
logging!(error, Type::Network, "No assertion found for Disney+");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (No Assertion)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let token_body = [
|
||||||
|
(
|
||||||
|
"grant_type",
|
||||||
|
"urn:ietf:params:oauth:grant-type:token-exchange",
|
||||||
|
),
|
||||||
|
("latitude", "0"),
|
||||||
|
("longitude", "0"),
|
||||||
|
("platform", "browser"),
|
||||||
|
("subject_token", assertion_str.as_str()),
|
||||||
|
(
|
||||||
|
"subject_token_type",
|
||||||
|
"urn:bamtech:params:oauth:token-type:device",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let token_result = client
|
||||||
|
.post(token_url)
|
||||||
|
.header("authorization", auth_header)
|
||||||
|
.header("content-type", "application/x-www-form-urlencoded")
|
||||||
|
.form(&token_body)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if token_result.is_err() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response = match token_result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Disney+ token response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let token_status = token_response.status();
|
||||||
|
|
||||||
|
let token_body_text = match token_response.text().await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(_) => {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Error: Cannot read token response)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if token_body_text.contains("forbidden-location") || token_body_text.contains("403 ERROR") {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "No (IP Banned By Disney+)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_json: Result<serde_json::Value, _> = serde_json::from_str(&token_body_text);
|
||||||
|
|
||||||
|
let refresh_token = match token_json {
|
||||||
|
Ok(json) => json
|
||||||
|
.get("refresh_token")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string()),
|
||||||
|
Err(_) => match Regex::new(r#""refresh_token"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(refresh_token_re) => refresh_token_re
|
||||||
|
.captures(&token_body_text)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile refresh_token regex for Disney+: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if refresh_token.is_none() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: format!(
|
||||||
|
"Failed (Error: Cannot extract refresh token, status: {}, response: {})",
|
||||||
|
token_status.as_u16(),
|
||||||
|
token_body_text.chars().take(100).collect::<String>() + "..."
|
||||||
|
),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let graphql_url = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql";
|
||||||
|
|
||||||
|
let graphql_payload = format!(
|
||||||
|
r#"{{"query":"mutation refreshToken($input: RefreshTokenInput!) {{ refreshToken(refreshToken: $input) {{ activeSession {{ sessionId }} }} }}","variables":{{"input":{{"refreshToken":"{}"}}}}}}"#,
|
||||||
|
refresh_token.unwrap_or_default()
|
||||||
|
);
|
||||||
|
|
||||||
|
let graphql_result = client
|
||||||
|
.post(graphql_url)
|
||||||
|
.header("authorization", auth_header)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(graphql_payload)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if graphql_result.is_err() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview_check = client.get("https://disneyplus.com").send().await;
|
||||||
|
|
||||||
|
let is_unavailable = match preview_check {
|
||||||
|
Ok(response) => {
|
||||||
|
let url = response.url().to_string();
|
||||||
|
url.contains("preview") || url.contains("unavailable")
|
||||||
|
}
|
||||||
|
Err(_) => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let graphql_response = match graphql_result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Disney+ GraphQL response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let graphql_status = graphql_response.status();
|
||||||
|
let graphql_body_text = match graphql_response.text().await {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to read Disney+ GraphQL response text: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
|
||||||
|
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||||
|
Ok(response) => match response.text().await {
|
||||||
|
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(region_re) => region_re
|
||||||
|
.captures(&body)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ main page region regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(region) = region_from_main {
|
||||||
|
let emoji = country_code_to_emoji(®ion);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region} (from main page)")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if graphql_body_text.is_empty() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: format!(
|
||||||
|
"Failed (GraphQL error: empty response, status: {})",
|
||||||
|
graphql_status.as_u16()
|
||||||
|
),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: format!(
|
||||||
|
"Failed (GraphQL error: {}, status: {})",
|
||||||
|
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
||||||
|
graphql_status.as_u16()
|
||||||
|
),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ countryCode regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let region_code = region_re
|
||||||
|
.captures(&graphql_body_text)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||||
|
|
||||||
|
let supported_re = match Regex::new(r#""inSupportedLocation"\s*:\s*(false|true)"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ supported location regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let in_supported_location = supported_re
|
||||||
|
.captures(&graphql_body_text)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
|
||||||
|
|
||||||
|
if region_code.is_none() {
|
||||||
|
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||||
|
Ok(response) => match response.text().await {
|
||||||
|
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||||
|
Ok(region_re) => region_re
|
||||||
|
.captures(&body)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Disney+ main page region regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(region) = region_from_main {
|
||||||
|
let emoji = country_code_to_emoji(®ion);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region} (from main page)")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let region = match region_code {
|
||||||
|
Some(code) => code,
|
||||||
|
None => {
|
||||||
|
logging!(error, Type::Network, "No region code found for Disney+");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if region == "JP" {
|
||||||
|
let emoji = country_code_to_emoji("JP");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region}")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_unavailable {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match in_supported_location {
|
||||||
|
Some(false) => {
|
||||||
|
let emoji = country_code_to_emoji(®ion);
|
||||||
|
UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Soon".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(true) => {
|
||||||
|
let emoji = country_code_to_emoji(®ion);
|
||||||
|
UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region}")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => UnlockItem {
|
||||||
|
name: "Disney+".to_string(),
|
||||||
|
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src-tauri/src/cmd/media_unlock_checker/gemini.rs
Normal file
66
src-tauri/src/cmd/media_unlock_checker/gemini.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_gemini(client: &Client) -> UnlockItem {
|
||||||
|
let url = "https://gemini.google.com";
|
||||||
|
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Ok(body) = response.text().await {
|
||||||
|
let is_ok = body.contains("45631641,null,true");
|
||||||
|
let status = if is_ok { "Yes" } else { "No" };
|
||||||
|
|
||||||
|
let re = match Regex::new(r#",2,1,200,"([A-Z]{3})""#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Gemini regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Gemini".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let region = re.captures(&body).and_then(|caps| {
|
||||||
|
caps.get(1).map(|m| {
|
||||||
|
let country_code = m.as_str();
|
||||||
|
let emoji = country_code_to_emoji(country_code);
|
||||||
|
format!("{emoji}{country_code}")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "Gemini".to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
region,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UnlockItem {
|
||||||
|
name: "Gemini".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "Gemini".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src-tauri/src/cmd/media_unlock_checker/mod.rs
Normal file
155
src-tauri/src/cmd/media_unlock_checker/mod.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use reqwest::Client;
|
||||||
|
use tauri::command;
|
||||||
|
use tokio::{sync::Mutex, task::JoinSet};
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
mod bahamut;
|
||||||
|
mod bilibili;
|
||||||
|
mod chatgpt;
|
||||||
|
mod disney_plus;
|
||||||
|
mod gemini;
|
||||||
|
mod netflix;
|
||||||
|
mod prime_video;
|
||||||
|
mod types;
|
||||||
|
mod utils;
|
||||||
|
mod youtube;
|
||||||
|
|
||||||
|
pub use types::UnlockItem;
|
||||||
|
|
||||||
|
use bahamut::check_bahamut_anime;
|
||||||
|
use bilibili::{check_bilibili_china_mainland, check_bilibili_hk_mc_tw};
|
||||||
|
use chatgpt::check_chatgpt_combined;
|
||||||
|
use disney_plus::check_disney_plus;
|
||||||
|
use gemini::check_gemini;
|
||||||
|
use netflix::check_netflix;
|
||||||
|
use prime_video::check_prime_video;
|
||||||
|
use youtube::check_youtube_premium;
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn get_unlock_items() -> Result<Vec<UnlockItem>, String> {
|
||||||
|
Ok(types::default_unlock_items())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||||
|
let client = match Client::builder()
|
||||||
|
.user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.danger_accept_invalid_hostnames(true)
|
||||||
|
.tcp_keepalive(std::time::Duration::from_secs(60))
|
||||||
|
.connection_verbose(true)
|
||||||
|
.build() {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => return Err(format!("创建HTTP客户端失败: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let results = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let mut tasks = JoinSet::new();
|
||||||
|
let client_arc = Arc::new(client);
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_bilibili_china_mainland(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_bilibili_hk_mc_tw(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let chatgpt_results = check_chatgpt_combined(&client).await;
|
||||||
|
let mut results = results.lock().await;
|
||||||
|
results.extend(chatgpt_results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_gemini(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_youtube_premium(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_bahamut_anime(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_netflix(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_disney_plus(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = Arc::clone(&client_arc);
|
||||||
|
let results = Arc::clone(&results);
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let result = check_prime_video(&client).await;
|
||||||
|
results.lock().await.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(res) = tasks.join_next().await {
|
||||||
|
if let Err(e) = res {
|
||||||
|
eprintln!("任务执行失败: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = match Arc::try_unwrap(results) {
|
||||||
|
Ok(mutex) => mutex.into_inner(),
|
||||||
|
Err(_) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to unwrap results Arc, references still exist"
|
||||||
|
);
|
||||||
|
return Err("Failed to collect results".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
220
src-tauri/src/cmd/media_unlock_checker/netflix.rs
Normal file
220
src-tauri/src/cmd/media_unlock_checker/netflix.rs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
use reqwest::Client;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||||
|
let cdn_result = check_netflix_cdn(client).await;
|
||||||
|
if cdn_result.status == "Yes" {
|
||||||
|
return cdn_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url1 = "https://www.netflix.com/title/81280792";
|
||||||
|
let url2 = "https://www.netflix.com/title/70143836";
|
||||||
|
|
||||||
|
let result1 = client
|
||||||
|
.get(url1)
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result1 {
|
||||||
|
eprintln!("Netflix请求错误: {e}");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let result2 = client
|
||||||
|
.get(url2)
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = &result2 {
|
||||||
|
eprintln!("Netflix请求错误: {e}");
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let status1 = match result1 {
|
||||||
|
Ok(response) => response.status().as_u16(),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Netflix response 1: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let status2 = match result2 {
|
||||||
|
Ok(response) => response.status().as_u16(),
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Netflix response 2: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if status1 == 404 && status2 == 404 {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Originals Only".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if status1 == 403 || status2 == 403 {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if status1 == 200 || status1 == 301 || status2 == 200 || status2 == 301 {
|
||||||
|
let test_url = "https://www.netflix.com/title/80018499";
|
||||||
|
match client
|
||||||
|
.get(test_url)
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => {
|
||||||
|
if let Some(location) = response.headers().get("location")
|
||||||
|
&& let Ok(location_str) = location.to_str()
|
||||||
|
{
|
||||||
|
let parts: Vec<&str> = location_str.split('/').collect();
|
||||||
|
if parts.len() >= 4 {
|
||||||
|
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
||||||
|
let emoji = country_code_to_emoji(region_code);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region_code}")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let emoji = country_code_to_emoji("us");
|
||||||
|
UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{}", "us")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("获取Netflix区域信息失败: {e}");
|
||||||
|
UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Yes (但无法获取区域)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||||
|
let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5";
|
||||||
|
|
||||||
|
match client
|
||||||
|
.get(url)
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => {
|
||||||
|
if response.status().as_u16() == 403 {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "No (IP Banned By Netflix)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match response.json::<Value>().await {
|
||||||
|
Ok(data) => {
|
||||||
|
if let Some(targets) = data.get("targets").and_then(|t| t.as_array())
|
||||||
|
&& !targets.is_empty()
|
||||||
|
&& let Some(location) = targets[0].get("location")
|
||||||
|
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
|
||||||
|
{
|
||||||
|
let emoji = country_code_to_emoji(country);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{country}")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Unknown".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("解析Fast.com API响应失败: {e}");
|
||||||
|
UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed (解析错误)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Fast.com API请求失败: {e}");
|
||||||
|
UnlockItem {
|
||||||
|
name: "Netflix".to_string(),
|
||||||
|
status: "Failed (CDN API)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src-tauri/src/cmd/media_unlock_checker/prime_video.rs
Normal file
108
src-tauri/src/cmd/media_unlock_checker/prime_video.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||||
|
let url = "https://www.primevideo.com";
|
||||||
|
|
||||||
|
let result = client.get(url).send().await;
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = match result {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to get Prime Video response: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Network Connection)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match response.text().await {
|
||||||
|
Ok(body) => {
|
||||||
|
let is_blocked = body.contains("isServiceRestricted");
|
||||||
|
|
||||||
|
let region_re = match Regex::new(r#""currentTerritory":"([^"]+)""#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile Prime Video region regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Regex Error)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let region_code = region_re
|
||||||
|
.captures(&body)
|
||||||
|
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||||
|
|
||||||
|
if is_blocked {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "No (Service Not Available)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(region) = region_code {
|
||||||
|
let emoji = country_code_to_emoji(®ion);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region: Some(format!("{emoji}{region}")),
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_blocked {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Error: PAGE ERROR)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Error: Unknown Region)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "Prime Video".to_string(),
|
||||||
|
status: "Failed (Error: Cannot read response)".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src-tauri/src/cmd/media_unlock_checker/types.rs
Normal file
40
src-tauri/src/cmd/media_unlock_checker/types.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UnlockItem {
|
||||||
|
pub name: String,
|
||||||
|
pub status: String,
|
||||||
|
pub region: Option<String>,
|
||||||
|
pub check_time: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnlockItem {
|
||||||
|
pub fn pending(name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
status: "Pending".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 10] = [
|
||||||
|
"哔哩哔哩大陆",
|
||||||
|
"哔哩哔哩港澳台",
|
||||||
|
"ChatGPT iOS",
|
||||||
|
"ChatGPT Web",
|
||||||
|
"Gemini",
|
||||||
|
"Youtube Premium",
|
||||||
|
"Bahamut Anime",
|
||||||
|
"Netflix",
|
||||||
|
"Disney+",
|
||||||
|
"Prime Video",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn default_unlock_items() -> Vec<UnlockItem> {
|
||||||
|
DEFAULT_UNLOCK_ITEM_NAMES
|
||||||
|
.iter()
|
||||||
|
.map(|name| UnlockItem::pending(name))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
21
src-tauri/src/cmd/media_unlock_checker/utils.rs
Normal file
21
src-tauri/src/cmd/media_unlock_checker/utils.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
pub fn get_local_date_string() -> String {
|
||||||
|
let now = Local::now();
|
||||||
|
now.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn country_code_to_emoji(country_code: &str) -> String {
|
||||||
|
let country_code = country_code.to_uppercase();
|
||||||
|
if country_code.len() < 2 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = country_code.as_bytes();
|
||||||
|
let c1 = 0x1F1E6 + (bytes[0] as u32) - ('A' as u32);
|
||||||
|
let c2 = 0x1F1E6 + (bytes[1] as u32) - ('A' as u32);
|
||||||
|
|
||||||
|
char::from_u32(c1)
|
||||||
|
.and_then(|c1| char::from_u32(c2).map(|c2| format!("{c1}{c2}")))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
82
src-tauri/src/cmd/media_unlock_checker/youtube.rs
Normal file
82
src-tauri/src/cmd/media_unlock_checker/youtube.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{logging, utils::logging::Type};
|
||||||
|
|
||||||
|
use super::UnlockItem;
|
||||||
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
|
pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||||
|
let url = "https://www.youtube.com/premium";
|
||||||
|
|
||||||
|
match client.get(url).send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
if let Ok(body) = response.text().await {
|
||||||
|
let body_lower = body.to_lowercase();
|
||||||
|
|
||||||
|
if body_lower.contains("youtube premium is not available in your country") {
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "No".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if body_lower.contains("ad-free") {
|
||||||
|
let re = match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||||
|
Ok(re) => re,
|
||||||
|
Err(e) => {
|
||||||
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Network,
|
||||||
|
"Failed to compile YouTube Premium regex: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let region = re.captures(&body).and_then(|caps| {
|
||||||
|
caps.get(1).map(|m| {
|
||||||
|
let country_code = m.as_str().trim();
|
||||||
|
let emoji = country_code_to_emoji(country_code);
|
||||||
|
format!("{emoji}{country_code}")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "Yes".to_string(),
|
||||||
|
region,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => UnlockItem {
|
||||||
|
name: "Youtube Premium".to_string(),
|
||||||
|
status: "Failed".to_string(),
|
||||||
|
region: None,
|
||||||
|
check_time: Some(get_local_date_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user