From 965ee9844d9944898c90ff2383b93206ac04620c Mon Sep 17 00:00:00 2001 From: Sline Date: Mon, 13 Oct 2025 18:56:15 +0800 Subject: [PATCH] refactor(unlock): restructure media unlock checker (#5044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- src-tauri/src/cmd/media_unlock_checker.rs | 1576 ----------------- .../src/cmd/media_unlock_checker/bahamut.rs | 124 ++ .../src/cmd/media_unlock_checker/bilibili.rs | 91 + .../src/cmd/media_unlock_checker/chatgpt.rs | 94 + .../cmd/media_unlock_checker/disney_plus.rs | 489 +++++ .../src/cmd/media_unlock_checker/gemini.rs | 66 + src-tauri/src/cmd/media_unlock_checker/mod.rs | 155 ++ .../src/cmd/media_unlock_checker/netflix.rs | 220 +++ .../cmd/media_unlock_checker/prime_video.rs | 108 ++ .../src/cmd/media_unlock_checker/types.rs | 40 + .../src/cmd/media_unlock_checker/utils.rs | 21 + .../src/cmd/media_unlock_checker/youtube.rs | 82 + 12 files changed, 1490 insertions(+), 1576 deletions(-) delete mode 100644 src-tauri/src/cmd/media_unlock_checker.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/bahamut.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/bilibili.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/chatgpt.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/disney_plus.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/gemini.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/mod.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/netflix.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/prime_video.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/types.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/utils.rs create mode 100644 src-tauri/src/cmd/media_unlock_checker/youtube.rs diff --git a/src-tauri/src/cmd/media_unlock_checker.rs b/src-tauri/src/cmd/media_unlock_checker.rs deleted file mode 100644 index 9bccc46bc..000000000 --- a/src-tauri/src/cmd/media_unlock_checker.rs +++ /dev/null @@ -1,1576 +0,0 @@ -use crate::{logging, utils::logging::Type}; -use chrono::Local; -use regex::Regex; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::Arc}; -use tauri::command; -use tokio::{sync::Mutex, task::JoinSet}; - -// 定义解锁测试项目的结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UnlockItem { - name: String, - status: String, - region: Option, - check_time: Option, -} - -// 获取当前本地时间字符串 -fn get_local_date_string() -> String { - let now = Local::now(); - now.format("%Y-%m-%d %H:%M:%S").to_string() -} - -// 将国家代码转换为对应的emoji -fn country_code_to_emoji(country_code: &str) -> String { - // 转换为大写 - let country_code = country_code.to_uppercase(); - - // 确保使用国家代码的前两个字符来生成emoji - if country_code.len() < 2 { - return String::new(); - } - - // 使用前两个字符生成emoji - 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() -} - -// 测试哔哩哔哩中国大陆 -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"; - - let result = client.get(url).send().await; - - match result { - Ok(response) => match response.json::().await { - Ok(body) => { - if let Some(code) = body.get("code").and_then(|v| v.as_i64()) { - let status = if code == 0 { - "Yes" - } else if code == -10403 { - "No" - } else { - "Failed" - }; - - UnlockItem { - name: "哔哩哔哩大陆".to_string(), - status: status.to_string(), - region: None, - check_time: Some(get_local_date_string()), - } - } else { - 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()), - }, - }, - Err(_) => UnlockItem { - name: "哔哩哔哩大陆".to_string(), - status: "Failed".to_string(), - region: None, - check_time: Some(get_local_date_string()), - }, - } -} - -// 测试哔哩哔哩港澳台 -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"; - - let result = client.get(url).send().await; - - match result { - Ok(response) => match response.json::().await { - Ok(body) => { - if let Some(code) = body.get("code").and_then(|v| v.as_i64()) { - let status = if code == 0 { - "Yes" - } else if code == -10403 { - "No" - } else { - "Failed" - }; - - UnlockItem { - name: "哔哩哔哩港澳台".to_string(), - status: status.to_string(), - region: None, - check_time: Some(get_local_date_string()), - } - } else { - 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()), - }, - }, - Err(_) => UnlockItem { - name: "哔哩哔哩港澳台".to_string(), - status: "Failed".to_string(), - region: None, - check_time: Some(get_local_date_string()), - }, - } -} - -// 合并的ChatGPT检测功能,包含iOS和Web测试以及国家代码获取 -async fn check_chatgpt_combined(client: &Client) -> Vec { - // 结果集 - let mut results = Vec::new(); - - // 1. 获取国家代码 - 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[0..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, - }; - - // 2. 测试 ChatGPT iOS - let url_ios = "https://ios.chat.openai.com/"; - let result_ios = client.get(url_ios).send().await; - - // 解析iOS测试结果 - 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", - }; - - // 3. 测试 ChatGPT Web - let url_web = "https://api.openai.com/compliance/cookie_requirements"; - let result_web = client.get(url_web).send().await; - - // 解析Web测试结果 - 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", - }; - - // 添加iOS测试结果 - results.push(UnlockItem { - name: "ChatGPT iOS".to_string(), - status: ios_status.to_string(), - region: region.clone(), - check_time: Some(get_local_date_string()), - }); - - // 添加Web测试结果 - results.push(UnlockItem { - name: "ChatGPT Web".to_string(), - status: web_status.to_string(), - region, - check_time: Some(get_local_date_string()), - }); - - results -} - -// 测试Gemini -async fn check_gemini(client: &Client) -> UnlockItem { - let url = "https://gemini.google.com"; - - let result = client.get(url).send().await; - - match result { - 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()), - }, - } -} - -// 测试 YouTube Premium -async fn check_youtube_premium(client: &Client) -> UnlockItem { - let url = "https://www.youtube.com/premium"; - - let result = client.get(url).send().await; - - match result { - 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") { - UnlockItem { - name: "Youtube Premium".to_string(), - status: "No".to_string(), - region: None, - check_time: Some(get_local_date_string()), - } - } else 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}") - }) - }); - - UnlockItem { - name: "Youtube Premium".to_string(), - status: "Yes".to_string(), - region, - 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()), - } - } - } 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()), - }, - } -} - -// 测试动画疯(Bahamut Anime) -async fn check_bahamut_anime(client: &Client) -> UnlockItem { - // 创建带Cookie存储的客户端 - let cookie_store = Arc::new(reqwest::cookie::Jar::default()); - - // 使用带Cookie的客户端 - let client_with_cookies = match reqwest::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() - } - }; - - // 第一步:获取设备ID (会自动保存Cookie) - 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) => { - // 使用正则提取deviceid - 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()), - }; - } - - // 第二步:使用设备ID检查访问权限 (使用相同的Cookie) - 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) => { - // 检查内容是否可访问 - 更精确地匹配animeSn - if body.contains("animeSn") { - Some(body) - } else { - None - } - } - Err(_) => None, - } - } - Err(_) => None, - }; - - // 如果无法获取token或不包含animeSn,表示不支持 - 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()), - }; - } - - // 第三步:访问主页获取区域信息 (使用相同的Cookie) - 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()), - } -} - -// 测试 Netflix -async fn check_netflix(client: &Client) -> UnlockItem { - // 首先尝试使用Fast.com API检测Netflix CDN区域 - let cdn_result = check_netflix_cdn(client).await; - if cdn_result.status == "Yes" { - return cdn_result; - } - - // 如果CDN方法失败,尝试传统的内容检测方法 - // 测试两个 Netflix 内容 (LEGO Ninjago 和 Breaking Bad) - let url1 = "https://www.netflix.com/title/81280792"; // LEGO Ninjago - let url2 = "https://www.netflix.com/title/70143836"; // Breaking Bad - - // 创建简单的请求(不添加太多头部信息) - 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 { - // 成功解锁,尝试获取地区信息 - // 使用Netflix测试内容获取区域 - 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()), - } - } -} - -// 使用Fast.com API检测Netflix CDN区域 -async fn check_netflix_cdn(client: &Client) -> UnlockItem { - // Fast.com API URL - let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5"; - - let result = client - .get(url) - .timeout(std::time::Duration::from_secs(30)) - .send() - .await; - - match result { - 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::().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()), - } - } - } -} - -// 测试 Disney+ -async fn check_disney_plus(client: &Client) -> UnlockItem { - // Disney+ 不支持 IPv6,但这里不做额外检查,因为我们使用的是系统默认网络 - - // 第一步:获取 assertion - 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()), - }; - } - }; - - // 检查是否 403 错误 - 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()), - }; - } - }; - - // 提取 assertion - 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()), - }; - } - - // 第二步:获取 token - let token_url = "https://disney.api.edge.bamgrid.com/token"; - - // 构建请求体 - 使用表单数据格式而非 JSON - 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) // 使用 form 而不是 json - .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()), - }; - } - - // 尝试解析 JSON - let token_json: Result = 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(_) => { - // 如果 JSON 解析失败,尝试使用正则表达式 - 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 - } - } - } - }; - - // 如果仍然无法获取 refresh token - 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::() + "..." - ), - region: None, - check_time: Some(get_local_date_string()), - }; - } - - // 第三步:使用 GraphQL 获取地区信息 - let graphql_url = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql"; - - // GraphQL API 通常接受 JSON 格式 - 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()), - }; - } - - // 检查 Disney+ 主页的可用性 - 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, - }; - - // 解析 GraphQL 响应获取区域信息 - 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() - } - }; - - // 如果 GraphQL 响应为空或明显错误,尝试直接获取区域信息 - 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::() + "...", - 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()), - }, - } -} - -// 测试 Amazon Prime Video -async fn check_prime_video(client: &Client) -> UnlockItem { - // 访问 Prime Video 主页 - 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 && region_code.is_none() { - 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()), - }, - } -} - -// 获取所有解锁项目的列表 -#[command] -pub async fn get_unlock_items() -> Result, String> { - let items = vec![ - UnlockItem { - name: "哔哩哔哩大陆".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "哔哩哔哩港澳台".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "ChatGPT iOS".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "ChatGPT Web".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "Gemini".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "Youtube Premium".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "Bahamut Anime".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "Netflix".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "Disney+".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - UnlockItem { - name: "Prime Video".to_string(), - status: "Pending".to_string(), - region: None, - check_time: None, - }, - ]; - - Ok(items) -} - -// 开始检测流媒体解锁状态 -#[command] -pub async fn check_media_unlock() -> Result, String> { - // 创建一个http客户端,增加更多配置 - 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) // 接受无效证书,防止SSL错误 - .danger_accept_invalid_hostnames(true) // 接受无效主机名 - .tcp_keepalive(std::time::Duration::from_secs(60)) // TCP keepalive - .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(); - - // 共享的Client实例 - 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; - let mut results = results.lock().await; - results.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; - let mut results = results.lock().await; - results.push(result); - }); - } - - // 添加合并的ChatGPT检测任务 - { - 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); - }); - } - - // 添加Gemini检测任务 - { - let client = Arc::clone(&client_arc); - let results = Arc::clone(&results); - tasks.spawn(async move { - let result = check_gemini(&client).await; - let mut results = results.lock().await; - results.push(result); - }); - } - - // 添加YouTube Premium检测任务 - { - let client = Arc::clone(&client_arc); - let results = Arc::clone(&results); - tasks.spawn(async move { - let result = check_youtube_premium(&client).await; - let mut results = results.lock().await; - results.push(result); - }); - } - - // 添加动画疯检测任务 - { - let client = Arc::clone(&client_arc); - let results = Arc::clone(&results); - tasks.spawn(async move { - let result = check_bahamut_anime(&client).await; - let mut results = results.lock().await; - results.push(result); - }); - } - - // 添加 Netflix 检测任务 - { - let client = Arc::clone(&client_arc); - let results = Arc::clone(&results); - tasks.spawn(async move { - let result = check_netflix(&client).await; - let mut results = results.lock().await; - results.push(result); - }); - } - - // 添加 Disney+ 检测任务 - { - let client = Arc::clone(&client_arc); - let results = Arc::clone(&results); - tasks.spawn(async move { - let result = check_disney_plus(&client).await; - let mut results = results.lock().await; - results.push(result); - }); - } - - // 添加 Prime Video 检测任务 - { - let client = Arc::clone(&client_arc); - let results = Arc::clone(&results); - tasks.spawn(async move { - let result = check_prime_video(&client).await; - let mut results = results.lock().await; - results.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) -} diff --git a/src-tauri/src/cmd/media_unlock_checker/bahamut.rs b/src-tauri/src/cmd/media_unlock_checker/bahamut.rs new file mode 100644 index 000000000..960011745 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/bahamut.rs @@ -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()), + } +} diff --git a/src-tauri/src/cmd/media_unlock_checker/bilibili.rs b/src-tauri/src/cmd/media_unlock_checker/bilibili.rs new file mode 100644 index 000000000..72c8697c5 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/bilibili.rs @@ -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::().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::().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()), + }, + } +} diff --git a/src-tauri/src/cmd/media_unlock_checker/chatgpt.rs b/src-tauri/src/cmd/media_unlock_checker/chatgpt.rs new file mode 100644 index 000000000..8c441a345 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/chatgpt.rs @@ -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 { + 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 +} diff --git a/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs b/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs new file mode 100644 index 000000000..d13133f2e --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/disney_plus.rs @@ -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::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::() + "..." + ), + 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::() + "...", + 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()), + }, + } +} diff --git a/src-tauri/src/cmd/media_unlock_checker/gemini.rs b/src-tauri/src/cmd/media_unlock_checker/gemini.rs new file mode 100644 index 000000000..4701bb6ef --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/gemini.rs @@ -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()), + }, + } +} diff --git a/src-tauri/src/cmd/media_unlock_checker/mod.rs b/src-tauri/src/cmd/media_unlock_checker/mod.rs new file mode 100644 index 000000000..8026057d1 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/mod.rs @@ -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, String> { + Ok(types::default_unlock_items()) +} + +#[command] +pub async fn check_media_unlock() -> Result, 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) +} diff --git a/src-tauri/src/cmd/media_unlock_checker/netflix.rs b/src-tauri/src/cmd/media_unlock_checker/netflix.rs new file mode 100644 index 000000000..bc1bc30b2 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/netflix.rs @@ -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::().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()), + } + } + } +} diff --git a/src-tauri/src/cmd/media_unlock_checker/prime_video.rs b/src-tauri/src/cmd/media_unlock_checker/prime_video.rs new file mode 100644 index 000000000..fb76723d1 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/prime_video.rs @@ -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()), + }, + } +} diff --git a/src-tauri/src/cmd/media_unlock_checker/types.rs b/src-tauri/src/cmd/media_unlock_checker/types.rs new file mode 100644 index 000000000..32c4990b4 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/types.rs @@ -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, + pub check_time: Option, +} + +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 { + DEFAULT_UNLOCK_ITEM_NAMES + .iter() + .map(|name| UnlockItem::pending(name)) + .collect() +} diff --git a/src-tauri/src/cmd/media_unlock_checker/utils.rs b/src-tauri/src/cmd/media_unlock_checker/utils.rs new file mode 100644 index 000000000..c1852b4d8 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/utils.rs @@ -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() +} diff --git a/src-tauri/src/cmd/media_unlock_checker/youtube.rs b/src-tauri/src/cmd/media_unlock_checker/youtube.rs new file mode 100644 index 000000000..26b437f04 --- /dev/null +++ b/src-tauri/src/cmd/media_unlock_checker/youtube.rs @@ -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()), + }, + } +}