region-restriction-checker/src/main.rs

378 lines
7.0 KiB
Rust

use std::{env, io};
use actix_web::{App, get, HttpResponse, HttpServer, Responder, web};
use async_recursion::async_recursion;
use lazy_static::lazy_static;
use redis::AsyncCommands;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[tokio::main]
async fn main() -> io::Result<()> {
let redis_addr = env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let redis = redis::Client::open(redis_addr).unwrap();
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(redis.clone()))
.service(check)
}).bind(("0.0.0.0", 8080))?
.run()
.await
}
#[derive(Deserialize)]
struct VideoIdQuery {
pub video_id: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct RestrictionInfo {
pub restricted: bool,
pub regions: Option<Vec<String>>,
}
lazy_static!(
static ref RE_VIDEO_ID: Regex = regex::Regex::new(r"^[a-zA-Z0-9_-]{11}$").unwrap();
static ref CLIENT: reqwest::Client = reqwest::ClientBuilder::new()
.user_agent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
.proxy(reqwest::Proxy::all(env::var("PROXY").unwrap_or_else(|_| "socks5://127.0.0.1:9150".to_string())).unwrap())
.build()
.unwrap();
);
const ALL_REGIONS: [&str; 244] = [
"AD",
"AE",
"AF",
"AG",
"AI",
"AL",
"AM",
"AO",
"AQ",
"AR",
"AS",
"AT",
"AU",
"AW",
"AX",
"AZ",
"BA",
"BB",
"BD",
"BE",
"BF",
"BG",
"BH",
"BI",
"BJ",
"BL",
"BM",
"BN",
"BO",
"BR",
"BS",
"BT",
"BV",
"BW",
"BY",
"BZ",
"CA",
"CC",
"CD",
"CF",
"CG",
"CH",
"CI",
"CK",
"CL",
"CM",
"CN",
"CO",
"CR",
"CU",
"CV",
"CX",
"CY",
"CZ",
"DE",
"DJ",
"DK",
"DM",
"DO",
"DZ",
"EC",
"EE",
"EG",
"EH",
"ER",
"ES",
"ET",
"FI",
"FJ",
"FK",
"FM",
"FO",
"FR",
"GA",
"GB",
"GD",
"GE",
"GF",
"GG",
"GH",
"GI",
"GL",
"GM",
"GN",
"GP",
"GQ",
"GR",
"GS",
"GT",
"GU",
"GW",
"GY",
"HK",
"HM",
"HN",
"HR",
"HT",
"HU",
"ID",
"IE",
"IL",
"IM",
"IN",
"IO",
"IQ",
"IR",
"IS",
"IT",
"JE",
"JM",
"JO",
"JP",
"KE",
"KG",
"KH",
"KI",
"KM",
"KN",
"KP",
"KR",
"KW",
"KY",
"KZ",
"LA",
"LB",
"LC",
"LI",
"LK",
"LR",
"LS",
"LT",
"LU",
"LV",
"LY",
"MA",
"MC",
"MD",
"ME",
"MG",
"MH",
"MK",
"ML",
"MM",
"MN",
"MO",
"MP",
"MQ",
"MR",
"MS",
"MT",
"MU",
"MV",
"MW",
"MX",
"MY",
"MZ",
"NA",
"NC",
"NE",
"NF",
"NG",
"NI",
"NL",
"NO",
"NP",
"NR",
"NU",
"NZ",
"OM",
"PA",
"PE",
"PF",
"PG",
"PH",
"PK",
"PL",
"PM",
"PN",
"PR",
"PS",
"PT",
"PW",
"PY",
"QA",
"RE",
"RO",
"RS",
"RU",
"RW",
"SA",
"SB",
"SC",
"SD",
"SE",
"SG",
"SH",
"SI",
"SJ",
"SK",
"SL",
"SM",
"SN",
"SO",
"SR",
"ST",
"SV",
"SY",
"SZ",
"TC",
"TD",
"TF",
"TG",
"TH",
"TJ",
"TK",
"TL",
"TM",
"TN",
"TO",
"TR",
"TT",
"TV",
"TW",
"TZ",
"UA",
"UG",
"UM",
"US",
"UY",
"UZ",
"VA",
"VC",
"VE",
"VG",
"VI",
"VN",
"VU",
"WF",
"WS",
"YE",
"YT",
"ZA",
"ZM",
"ZW"
];
#[async_recursion]
async fn fetch_restrictions(video_id: &str) -> RestrictionInfo {
let resp = CLIENT.get(format!("https://content-youtube.googleapis.com/youtube/v3/videos?id={video_id}&part=contentDetails&key=AIzaSyAa8yy0GdcGPHdtD083HiGGx_S0vMPScDM"))
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Referer", "https://content-youtube.googleapis.com/")
.header("X-ClientDetails", "appVersion=5.0%20(Windows)&platform=Win32&userAgent=Mozilla%2F5.0%20(Windows%20NT%2010.0%3B%20rv%3A91.0)%20Gecko%2F20100101%20Firefox%2F91.0")
.header("X-Requested-With", "XMLHttpRequest")
.header("X-JavaScript-User-Agent", "apix/3.0.0 google-api-javascript-client/1.1.0")
.header("X-Origin", "https://explorer.apis.google.com")
.header("X-Referer", "https://explorer.apis.google.com")
.header("X-Goog-Encode-Response-If-Executable", "base64")
.header("Connection", "close")
.header("Sec-Fetch-Dest", "empty")
.header("Sec-Fetch-Mode", "cors")
.header("Sec-Fetch-Site", "same-origin")
.header("TE", "trailers")
.send().await;
let resp = resp.unwrap();
if !resp.status().is_success() {
return fetch_restrictions(video_id).await;
}
let resp = resp.text().await.unwrap();
let resp: serde_json::Value = serde_json::from_str(&resp).unwrap();
let blocked = resp["items"][0]["contentDetails"]["regionRestriction"]["blocked"].as_array();
let allowed = resp["items"][0]["contentDetails"]["regionRestriction"]["allowed"].as_array();
let region = {
if let Some(allowed) = allowed {
let regions = allowed.iter().map(|x| x.as_str().unwrap().to_string()).collect();
RestrictionInfo {
restricted: true,
regions: Some(regions),
}
} else if let Some(blocked) = blocked {
let blocked: Vec<String> = blocked.iter().map(|x| x.as_str().unwrap().to_string()).collect();
let regions = ALL_REGIONS.iter().filter(|x| !blocked.contains(&x.to_string())).map(|x| x.to_string()).collect();
RestrictionInfo {
restricted: true,
regions: Some(regions),
}
} else {
RestrictionInfo {
restricted: false,
regions: None,
}
}
};
region
}
#[get("/api/region/check")]
async fn check(query: web::Query<VideoIdQuery>, redis: web::Data<redis::Client>) -> impl Responder {
let video_id = &query.video_id;
if !RE_VIDEO_ID.is_match(video_id) {
return HttpResponse::BadRequest()
.body("Invalid video ID");
}
let mut conn = redis.get_tokio_connection().await.unwrap();
let restrictions = conn.get::<_, String>(video_id).await;
let restrictions = {
if restrictions.is_ok() {
serde_json::from_str(restrictions.unwrap().as_str()).unwrap()
} else {
let restrictions = fetch_restrictions(video_id).await;
conn.set_ex::<_, String, ()>(video_id, serde_json::to_string(&restrictions).unwrap(), 3600 * 24 * 30).await
.expect("Failed to set restrictions in redis");
restrictions
}
};
HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::to_string(&restrictions).unwrap())
}