feat: add claude, spotify and tiktok into unlock checker

This commit is contained in:
Slinetrac
2025-10-13 23:55:40 +08:00
parent 15a0c30ccb
commit 1d725b8bde
6 changed files with 322 additions and 28 deletions

View File

@@ -0,0 +1,60 @@
use reqwest::Client;
use super::UnlockItem;
use super::utils::{country_code_to_emoji, get_local_date_string};
const BLOCKED_CODES: [&str; 10] = ["AF", "BY", "CN", "CU", "HK", "IR", "KP", "MO", "RU", "SY"];
pub(super) async fn check_claude(client: &Client) -> UnlockItem {
let url = "https://claude.ai/cdn-cgi/trace";
match client.get(url).send().await {
Ok(response) => match response.text().await {
Ok(body) => {
let mut country_code: Option<String> = None;
for line in body.lines() {
if let Some(rest) = line.strip_prefix("loc=") {
country_code = Some(rest.trim().to_uppercase());
break;
}
}
if let Some(code) = country_code {
let emoji = country_code_to_emoji(&code);
let status = if BLOCKED_CODES.contains(&code.as_str()) {
"No"
} else {
"Yes"
};
UnlockItem {
name: "Claude".to_string(),
status: status.to_string(),
region: Some(format!("{emoji}{code}")),
check_time: Some(get_local_date_string()),
}
} else {
UnlockItem {
name: "Claude".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
}
}
}
Err(_) => UnlockItem {
name: "Claude".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
},
Err(_) => UnlockItem {
name: "Claude".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
}
}

View File

@@ -9,10 +9,13 @@ use crate::{logging, utils::logging::Type};
mod bahamut;
mod bilibili;
mod chatgpt;
mod claude;
mod disney_plus;
mod gemini;
mod netflix;
mod prime_video;
mod spotify;
mod tiktok;
mod types;
mod utils;
mod youtube;
@@ -22,10 +25,13 @@ 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 claude::check_claude;
use disney_plus::check_disney_plus;
use gemini::check_gemini;
use netflix::check_netflix;
use prime_video::check_prime_video;
use spotify::check_spotify;
use tiktok::check_tiktok;
use youtube::check_youtube_premium;
#[command]
@@ -79,6 +85,15 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
});
}
{
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_claude(&client).await;
results.lock().await.push(result);
});
}
{
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
@@ -124,6 +139,24 @@ pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
});
}
{
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);
tasks.spawn(async move {
let result = check_spotify(&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_tiktok(&client).await;
results.lock().await.push(result);
});
}
{
let client = Arc::clone(&client_arc);
let results = Arc::clone(&results);

View File

@@ -0,0 +1,79 @@
use reqwest::{Client, Url};
use super::UnlockItem;
use super::utils::{country_code_to_emoji, get_local_date_string};
pub(super) async fn check_spotify(client: &Client) -> UnlockItem {
let url = "https://www.spotify.com/api/content/v1/country-selector?platform=web&format=json";
match client.get(url).send().await {
Ok(response) => {
let final_url = response.url().clone();
let status_code = response.status();
let body = response.text().await.unwrap_or_default();
let region = extract_region(&final_url).or_else(|| extract_region_from_body(&body));
let status = determine_status(status_code.as_u16(), &body);
UnlockItem {
name: "Spotify".to_string(),
status: status.to_string(),
region,
check_time: Some(get_local_date_string()),
}
}
Err(_) => UnlockItem {
name: "Spotify".to_string(),
status: "Failed".to_string(),
region: None,
check_time: Some(get_local_date_string()),
},
}
}
fn determine_status(status: u16, body: &str) -> &'static str {
if status == 403 || status == 451 {
return "No";
}
if !(200..300).contains(&status) {
return "Failed";
}
let body_lower = body.to_lowercase();
if body_lower.contains("not available in your country") {
return "No";
}
"Yes"
}
fn extract_region(url: &Url) -> Option<String> {
let mut segments = url.path_segments()?;
let first_segment = segments.next()?;
if first_segment.is_empty() || first_segment == "api" {
return None;
}
let country_code = first_segment.split('-').next().unwrap_or(first_segment);
let upper = country_code.to_uppercase();
let emoji = country_code_to_emoji(&upper);
Some(format!("{emoji}{upper}"))
}
fn extract_region_from_body(body: &str) -> Option<String> {
let marker = "\"countryCode\":\"";
if let Some(idx) = body.find(marker) {
let start = idx + marker.len();
let rest = &body[start..];
if let Some(end) = rest.find('"') {
let code = rest[..end].to_uppercase();
if !code.is_empty() {
let emoji = country_code_to_emoji(&code);
return Some(format!("{emoji}{code}"));
}
}
}
None
}

View File

@@ -0,0 +1,87 @@
use std::sync::OnceLock;
use regex::Regex;
use reqwest::Client;
use super::UnlockItem;
use super::utils::{country_code_to_emoji, get_local_date_string};
pub(super) async fn check_tiktok(client: &Client) -> UnlockItem {
let trace_url = "https://www.tiktok.com/cdn-cgi/trace";
let mut status = String::from("Failed");
let mut region = None;
if let Ok(response) = client.get(trace_url).send().await {
let status_code = response.status().as_u16();
if let Ok(body) = response.text().await {
status = determine_status(status_code, &body).to_string();
region = extract_region_from_body(&body);
}
}
if (region.is_none() || status == "Failed")
&& let Ok(response) = client.get("https://www.tiktok.com/").send().await
{
let status_code = response.status().as_u16();
if let Ok(body) = response.text().await {
let fallback_status = determine_status(status_code, &body);
let fallback_region = extract_region_from_body(&body);
if status != "No" {
status = fallback_status.to_string();
}
if region.is_none() {
region = fallback_region;
}
}
}
UnlockItem {
name: "TikTok".to_string(),
status,
region,
check_time: Some(get_local_date_string()),
}
}
fn determine_status(status: u16, body: &str) -> &'static str {
if status == 403 || status == 451 {
return "No";
}
if !(200..300).contains(&status) {
return "Failed";
}
let body_lower = body.to_lowercase();
if body_lower.contains("access denied")
|| body_lower.contains("not available in your region")
|| body_lower.contains("tiktok is not available")
{
return "No";
}
"Yes"
}
fn extract_region_from_body(body: &str) -> Option<String> {
static REGION_REGEX: OnceLock<Option<Regex>> = OnceLock::new();
let regex = REGION_REGEX
.get_or_init(|| Regex::new(r#""region"\s*:\s*"([a-zA-Z-]+)""#).ok())
.as_ref()?;
if let Some(caps) = regex.captures(body)
&& let Some(matched) = caps.get(1)
{
let raw = matched.as_str();
let country_code = raw.split('-').next().unwrap_or(raw).to_uppercase();
if !country_code.is_empty() {
let emoji = country_code_to_emoji(&country_code);
return Some(format!("{emoji}{country_code}"));
}
}
None
}

View File

@@ -19,17 +19,20 @@ impl UnlockItem {
}
}
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 10] = [
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 13] = [
"哔哩哔哩大陆",
"哔哩哔哩港澳台",
"ChatGPT iOS",
"ChatGPT Web",
"Claude",
"Gemini",
"Youtube Premium",
"Bahamut Anime",
"Netflix",
"Disney+",
"Prime Video",
"Spotify",
"TikTok",
];
pub fn default_unlock_items() -> Vec<UnlockItem> {

View File

@@ -49,19 +49,43 @@ const UnlockPage = () => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, []);
// 保存测试结果到本地存储
const saveResultsToStorage = (items: UnlockItem[], time: string | null) => {
try {
localStorage.setItem(UNLOCK_RESULTS_STORAGE_KEY, JSON.stringify(items));
if (time) {
localStorage.setItem(UNLOCK_RESULTS_TIME_KEY, time);
const mergeUnlockItems = useCallback(
(defaults: UnlockItem[], existing?: UnlockItem[] | null) => {
if (!existing || existing.length === 0) {
return defaults;
}
} catch (err) {
console.error("Failed to save results to storage:", err);
}
};
const loadResultsFromStorage = (): {
const existingMap = new Map(existing.map((item) => [item.name, item]));
const merged = defaults.map((item) => existingMap.get(item.name) ?? item);
const mergedNameSet = new Set(merged.map((item) => item.name));
existing.forEach((item) => {
if (!mergedNameSet.has(item.name)) {
merged.push(item);
}
});
return merged;
},
[],
);
// 保存测试结果到本地存储
const saveResultsToStorage = useCallback(
(items: UnlockItem[], time: string | null) => {
try {
localStorage.setItem(UNLOCK_RESULTS_STORAGE_KEY, JSON.stringify(items));
if (time) {
localStorage.setItem(UNLOCK_RESULTS_TIME_KEY, time);
}
} catch (err) {
console.error("Failed to save results to storage:", err);
}
},
[],
);
const loadResultsFromStorage = useCallback((): {
items: UnlockItem[] | null;
time: string | null;
} => {
@@ -80,34 +104,42 @@ const UnlockPage = () => {
}
return { items: null, time: null };
};
}, []);
const getUnlockItems = useCallback(
async (updateUI: boolean = true) => {
async (
existingItems: UnlockItem[] | null = null,
existingTime: string | null = null,
) => {
try {
const items = await invoke<UnlockItem[]>("get_unlock_items");
const sortedItems = sortItemsByName(items);
const defaultItems = await invoke<UnlockItem[]>("get_unlock_items");
const mergedItems = mergeUnlockItems(defaultItems, existingItems);
const sortedItems = sortItemsByName(mergedItems);
if (updateUI) {
setUnlockItems(sortedItems);
}
setUnlockItems(sortedItems);
saveResultsToStorage(
sortedItems,
existingItems && existingItems.length > 0 ? existingTime : null,
);
} catch (err: any) {
console.error("Failed to get unlock items:", err);
}
},
[sortItemsByName],
[mergeUnlockItems, saveResultsToStorage, sortItemsByName],
);
useEffect(() => {
const { items: storedItems } = loadResultsFromStorage();
void (async () => {
const { items: storedItems, time: storedTime } = loadResultsFromStorage();
if (storedItems && storedItems.length > 0) {
setUnlockItems(storedItems);
getUnlockItems(false);
} else {
getUnlockItems(true);
}
}, [getUnlockItems]);
if (storedItems && storedItems.length > 0) {
setUnlockItems(sortItemsByName(storedItems));
await getUnlockItems(storedItems, storedTime);
} else {
await getUnlockItems();
}
})();
}, [getUnlockItems, loadResultsFromStorage, sortItemsByName]);
const invokeWithTimeout = async <T,>(
cmd: string,