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 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);
|
||||
|
||||
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 Web",
|
||||
"Claude",
|
||||
"Gemini",
|
||||
"Youtube Premium",
|
||||
"Bahamut Anime",
|
||||
"Netflix",
|
||||
"Disney+",
|
||||
"Prime Video",
|
||||
"Spotify",
|
||||
"TikTok",
|
||||
];
|
||||
|
||||
pub fn default_unlock_items() -> Vec<UnlockItem> {
|
||||
|
||||
Reference in New Issue
Block a user