tiktok/
client.rs

1use crate::Error;
2use crate::FeedCursor;
3use rand::RngExt;
4use reqwest::header::HeaderMap;
5use reqwest::header::HeaderValue;
6use std::time::SystemTime;
7use url::Url;
8use uuid::Uuid;
9
10const API_HOST: &str = "api22-normal-c-useast2a.tiktokv.com";
11
12const USER_AGENT_STR: &str = "Mozilla/5.0";
13
14static ACCEPT_VALUE: HeaderValue = HeaderValue::from_static("*/*");
15static ACCEPT_ENCODING_VALUE: HeaderValue = HeaderValue::from_static("identity;q=1, *;q=0");
16static ACCEPT_LANGUAGE_VALUE: HeaderValue = HeaderValue::from_static("en-US,en;q=0.8");
17
18/// A tiktok client
19#[derive(Debug, Clone)]
20pub struct Client {
21    /// The inner HTTP client.
22    ///
23    /// Should only be used if you want to piggyback off of this for HTTP requests
24    pub client: reqwest::Client,
25}
26
27impl Client {
28    /// Make a new [`Client`].
29    pub fn new() -> Self {
30        let mut headers = HeaderMap::new();
31        headers.insert(reqwest::header::ACCEPT, ACCEPT_VALUE.clone());
32        headers.insert(
33            reqwest::header::ACCEPT_ENCODING,
34            ACCEPT_ENCODING_VALUE.clone(),
35        );
36        headers.insert(
37            reqwest::header::ACCEPT_LANGUAGE,
38            ACCEPT_LANGUAGE_VALUE.clone(),
39        );
40
41        let client = reqwest::Client::builder()
42            .user_agent(USER_AGENT_STR)
43            .cookie_store(true)
44            // .use_rustls_tls() // native-tls chokes for some reason
45            .default_headers(headers)
46            .build()
47            .expect("failed to build client");
48
49        Self { client }
50    }
51
52    /// Get a feed.
53    pub async fn get_feed(&self, video_id: Option<u64>) -> Result<FeedCursor, Error> {
54        // let app_name = "musical_ly";
55        let version_name = "34.1.2";
56        let version_code = "2023401020";
57
58        let url = format!("https://{API_HOST}/aweme/v1/feed/");
59        // This should always be valid
60        let mut url = Url::parse(&url).unwrap();
61        {
62            let mut rng = rand::rng();
63            let epoch_seconds = SystemTime::now()
64                .duration_since(SystemTime::UNIX_EPOCH)
65                .map(|duration| duration.as_secs())
66                .unwrap_or(0);
67            let hex_slice = [
68                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
69            ];
70            let hex_distribution = rand::distr::slice::Choose::new(&hex_slice).unwrap();
71
72            let mut query_pairs = url.query_pairs_mut();
73
74            if let Some(video_id) = video_id {
75                query_pairs.append_pair("aweme_id", itoa::Buffer::new().format(video_id));
76            }
77
78            query_pairs.append_pair("version_name", version_name);
79            query_pairs.append_pair("ab_version", version_name);
80            query_pairs.append_pair("version_code", version_code);
81            query_pairs.append_pair("build_number", version_name);
82            query_pairs.append_pair("manifest_version_code", version_code);
83            query_pairs.append_pair("update_version_code", version_code);
84
85            query_pairs.append_pair("iid", "7351149742343391009");
86            let device_id = rng.random_range(7250000000000000000_u64..7351147085025500000_u64);
87            query_pairs.append_pair("device_id", itoa::Buffer::new().format(device_id));
88            query_pairs.append_pair("region", "US");
89            query_pairs.append_pair("os", "android");
90            query_pairs.append_pair("device_type", "Pixel 7");
91            query_pairs.append_pair("device_brand", "Google");
92            query_pairs.append_pair("language", "en");
93            query_pairs.append_pair("os_version", "13");
94            query_pairs.append_pair("ts", itoa::Buffer::new().format(epoch_seconds));
95            let last_install_time = epoch_seconds.saturating_sub(rng.random_range(86400..1123200));
96            query_pairs.append_pair(
97                "last_install_time",
98                itoa::Buffer::new().format(last_install_time),
99            );
100            query_pairs.append_pair("_rticket", itoa::Buffer::new().format(epoch_seconds * 1000));
101            query_pairs.append_pair("channel", "googleplay");
102            let openudid: String = rng.sample_iter(hex_distribution).take(16).collect();
103            query_pairs.append_pair("openudid", openudid.as_str());
104            query_pairs.append_pair("aid", "0");
105            let cdid = Uuid::new_v4();
106            query_pairs.append_pair("cdid", &cdid.to_string());
107        }
108
109        let package = format!("com.zhiliaoapp.musically/{version_code}"); // com.ss.android.ugc.{app_name}/{version_code}
110        let user_agent = format!("{package} (Linux; U; Android 13; en_US; Pixel 7; Build/TD1A.220804.031; Cronet/58.0.2991.0)");
111
112        let json = self
113            .client
114            .get(url)
115            .header(reqwest::header::ACCEPT, "application/json")
116            .header(reqwest::header::USER_AGENT, user_agent)
117            .send()
118            .await?
119            .error_for_status()?
120            .json()
121            .await?;
122
123        Ok(json)
124    }
125}
126
127impl Default for Client {
128    fn default() -> Self {
129        Self::new()
130    }
131}