mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat: add claude, spotify and tiktok into unlock checker
This commit is contained in:
60
src-tauri/src/cmd/media_unlock_checker/claude.rs
Normal file
60
src-tauri/src/cmd/media_unlock_checker/claude.rs
Normal 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()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,13 @@ use crate::{logging, utils::logging::Type};
|
|||||||
mod bahamut;
|
mod bahamut;
|
||||||
mod bilibili;
|
mod bilibili;
|
||||||
mod chatgpt;
|
mod chatgpt;
|
||||||
|
mod claude;
|
||||||
mod disney_plus;
|
mod disney_plus;
|
||||||
mod gemini;
|
mod gemini;
|
||||||
mod netflix;
|
mod netflix;
|
||||||
mod prime_video;
|
mod prime_video;
|
||||||
|
mod spotify;
|
||||||
|
mod tiktok;
|
||||||
mod types;
|
mod types;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod youtube;
|
mod youtube;
|
||||||
@@ -22,10 +25,13 @@ pub use types::UnlockItem;
|
|||||||
use bahamut::check_bahamut_anime;
|
use bahamut::check_bahamut_anime;
|
||||||
use bilibili::{check_bilibili_china_mainland, check_bilibili_hk_mc_tw};
|
use bilibili::{check_bilibili_china_mainland, check_bilibili_hk_mc_tw};
|
||||||
use chatgpt::check_chatgpt_combined;
|
use chatgpt::check_chatgpt_combined;
|
||||||
|
use claude::check_claude;
|
||||||
use disney_plus::check_disney_plus;
|
use disney_plus::check_disney_plus;
|
||||||
use gemini::check_gemini;
|
use gemini::check_gemini;
|
||||||
use netflix::check_netflix;
|
use netflix::check_netflix;
|
||||||
use prime_video::check_prime_video;
|
use prime_video::check_prime_video;
|
||||||
|
use spotify::check_spotify;
|
||||||
|
use tiktok::check_tiktok;
|
||||||
use youtube::check_youtube_premium;
|
use youtube::check_youtube_premium;
|
||||||
|
|
||||||
#[command]
|
#[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 client = Arc::clone(&client_arc);
|
||||||
let results = Arc::clone(&results);
|
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 client = Arc::clone(&client_arc);
|
||||||
let results = Arc::clone(&results);
|
let results = Arc::clone(&results);
|
||||||
|
|||||||
79
src-tauri/src/cmd/media_unlock_checker/spotify.rs
Normal file
79
src-tauri/src/cmd/media_unlock_checker/spotify.rs
Normal 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
|
||||||
|
}
|
||||||
87
src-tauri/src/cmd/media_unlock_checker/tiktok.rs
Normal file
87
src-tauri/src/cmd/media_unlock_checker/tiktok.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -19,17 +19,20 @@ impl UnlockItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 10] = [
|
const DEFAULT_UNLOCK_ITEM_NAMES: [&str; 13] = [
|
||||||
"哔哩哔哩大陆",
|
"哔哩哔哩大陆",
|
||||||
"哔哩哔哩港澳台",
|
"哔哩哔哩港澳台",
|
||||||
"ChatGPT iOS",
|
"ChatGPT iOS",
|
||||||
"ChatGPT Web",
|
"ChatGPT Web",
|
||||||
|
"Claude",
|
||||||
"Gemini",
|
"Gemini",
|
||||||
"Youtube Premium",
|
"Youtube Premium",
|
||||||
"Bahamut Anime",
|
"Bahamut Anime",
|
||||||
"Netflix",
|
"Netflix",
|
||||||
"Disney+",
|
"Disney+",
|
||||||
"Prime Video",
|
"Prime Video",
|
||||||
|
"Spotify",
|
||||||
|
"TikTok",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn default_unlock_items() -> Vec<UnlockItem> {
|
pub fn default_unlock_items() -> Vec<UnlockItem> {
|
||||||
|
|||||||
@@ -49,19 +49,43 @@ const UnlockPage = () => {
|
|||||||
return [...items].sort((a, b) => a.name.localeCompare(b.name));
|
return [...items].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 保存测试结果到本地存储
|
const mergeUnlockItems = useCallback(
|
||||||
const saveResultsToStorage = (items: UnlockItem[], time: string | null) => {
|
(defaults: UnlockItem[], existing?: UnlockItem[] | null) => {
|
||||||
try {
|
if (!existing || existing.length === 0) {
|
||||||
localStorage.setItem(UNLOCK_RESULTS_STORAGE_KEY, JSON.stringify(items));
|
return defaults;
|
||||||
if (time) {
|
|
||||||
localStorage.setItem(UNLOCK_RESULTS_TIME_KEY, time);
|
|
||||||
}
|
}
|
||||||
} 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;
|
items: UnlockItem[] | null;
|
||||||
time: string | null;
|
time: string | null;
|
||||||
} => {
|
} => {
|
||||||
@@ -80,34 +104,42 @@ const UnlockPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { items: null, time: null };
|
return { items: null, time: null };
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getUnlockItems = useCallback(
|
const getUnlockItems = useCallback(
|
||||||
async (updateUI: boolean = true) => {
|
async (
|
||||||
|
existingItems: UnlockItem[] | null = null,
|
||||||
|
existingTime: string | null = null,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const items = await invoke<UnlockItem[]>("get_unlock_items");
|
const defaultItems = await invoke<UnlockItem[]>("get_unlock_items");
|
||||||
const sortedItems = sortItemsByName(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) {
|
} catch (err: any) {
|
||||||
console.error("Failed to get unlock items:", err);
|
console.error("Failed to get unlock items:", err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sortItemsByName],
|
[mergeUnlockItems, saveResultsToStorage, sortItemsByName],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { items: storedItems } = loadResultsFromStorage();
|
void (async () => {
|
||||||
|
const { items: storedItems, time: storedTime } = loadResultsFromStorage();
|
||||||
|
|
||||||
if (storedItems && storedItems.length > 0) {
|
if (storedItems && storedItems.length > 0) {
|
||||||
setUnlockItems(storedItems);
|
setUnlockItems(sortItemsByName(storedItems));
|
||||||
getUnlockItems(false);
|
await getUnlockItems(storedItems, storedTime);
|
||||||
} else {
|
} else {
|
||||||
getUnlockItems(true);
|
await getUnlockItems();
|
||||||
}
|
}
|
||||||
}, [getUnlockItems]);
|
})();
|
||||||
|
}, [getUnlockItems, loadResultsFromStorage, sortItemsByName]);
|
||||||
|
|
||||||
const invokeWithTimeout = async <T,>(
|
const invokeWithTimeout = async <T,>(
|
||||||
cmd: string,
|
cmd: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user