pane2k commited on
Commit
7e4b742
·
verified ·
1 Parent(s): 4f4d353

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +10 -0
  2. .gradio/certificate.pem +31 -0
  3. Data/JSON/TotalView.json +1 -0
  4. Data/JSON/blackList.json +9 -0
  5. Data/JSON/index.json +1 -0
  6. Data/TXT/Cacto0o.txt +14 -0
  7. Data/TXT/T2x2.txt +48 -0
  8. Html/index.html +104 -0
  9. Html/main.js +154 -0
  10. Html/reconnecting-websoket.min.js +365 -0
  11. Main.py +222 -0
  12. README.md +1 -7
  13. TikTok/Cookies/__init__.py +0 -0
  14. TikTok/Cookies/cookie.py +94 -0
  15. TikTok/Server/SaveTotalView.py +52 -0
  16. TikTok/Server/__init__.py +0 -0
  17. TikTok/Server/main.py +124 -0
  18. TikTok/Server/users.py +5 -0
  19. TikTok/Statistic/AsyncUser.py +183 -0
  20. TikTok/Statistic/SingleUser.py +179 -0
  21. TikTok/Statistic/__init__.py +0 -0
  22. TikTok/Statistic/tiktok.py +170 -0
  23. TikTok/TikTokApi/__init__.py +1 -0
  24. TikTok/TikTokApi/api/__init__.py +0 -0
  25. TikTok/TikTokApi/api/comment.py +92 -0
  26. TikTok/TikTokApi/api/hashtag.py +167 -0
  27. TikTok/TikTokApi/api/search.py +106 -0
  28. TikTok/TikTokApi/api/sound.py +179 -0
  29. TikTok/TikTokApi/api/trending.py +60 -0
  30. TikTok/TikTokApi/api/user.py +280 -0
  31. TikTok/TikTokApi/api/video.py +332 -0
  32. TikTok/TikTokApi/exceptions.py +35 -0
  33. TikTok/TikTokApi/helpers.py +36 -0
  34. TikTok/TikTokApi/stealth/__init__.py +1 -0
  35. TikTok/TikTokApi/stealth/js/__init__.py +0 -0
  36. TikTok/TikTokApi/stealth/js/chrome_app.py +73 -0
  37. TikTok/TikTokApi/stealth/js/chrome_csi.py +29 -0
  38. TikTok/TikTokApi/stealth/js/chrome_hairline.py +16 -0
  39. TikTok/TikTokApi/stealth/js/chrome_load_times.py +124 -0
  40. TikTok/TikTokApi/stealth/js/chrome_runtime.py +265 -0
  41. TikTok/TikTokApi/stealth/js/generate_magic_arrays.py +144 -0
  42. TikTok/TikTokApi/stealth/js/iframe_contentWindow.py +99 -0
  43. TikTok/TikTokApi/stealth/js/media_codecs.py +65 -0
  44. TikTok/TikTokApi/stealth/js/navigator_hardwareConcurrency.py +10 -0
  45. TikTok/TikTokApi/stealth/js/navigator_languages.py +6 -0
  46. TikTok/TikTokApi/stealth/js/navigator_permissions.py +22 -0
  47. TikTok/TikTokApi/stealth/js/navigator_platform.py +7 -0
  48. TikTok/TikTokApi/stealth/js/navigator_plugins.py +94 -0
  49. TikTok/TikTokApi/stealth/js/navigator_userAgent.py +8 -0
  50. TikTok/TikTokApi/stealth/js/navigator_vendor.py +6 -0
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv
2
+ __pycache__
3
+ TgBotToken.txt
4
+ TestTiktok
5
+ chromedriver-win64
6
+ Users
7
+ tests
8
+ a
9
+ b
10
+ cookies.json
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
Data/JSON/TotalView.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"total_views": 36725712, "total_videos_with_tag": 545}
Data/JSON/blackList.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "usernames": [
3
+ "pane2kvod"
4
+ ],
5
+ "videos":
6
+ [
7
+ 1111
8
+ ]
9
+ }
Data/JSON/index.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"parts": 1, "selectedPart": 0}
Data/TXT/Cacto0o.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pane2kvod
2
+ nestereko
3
+ tiltocacto0o
4
+ nnestereko
5
+ kakto_pane2k
6
+ fiintex
7
+ nepibaro
8
+ kakusnarezki
9
+ kudravie
10
+ _dinsa_
11
+ tartafogo
12
+ c_h_e_l_o_v_e_kk
13
+ melintaivottak
14
+ donlorrento
Data/TXT/T2x2.txt ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ shorts.t2x2
2
+ ptytok
3
+ sheh_blogerov
4
+ izvestniy.moments
5
+ futurenn
6
+ suharik_tani
7
+ t2x2rezki
8
+ makartalovvv
9
+ realt2x
10
+ t2x2clip
11
+ averagekittenfan
12
+ _ledifws
13
+ ssk1zyy
14
+ skrises
15
+ liv.tvv
16
+ ebat.ti.kto.man
17
+ moments_t2x2
18
+ t2x2cuts
19
+ smachnui_prikol
20
+ kyvalda4ka
21
+ t2x2tosha0
22
+ el.roflano
23
+ nevermor1ng
24
+ dosmis423789_t2x2stintik
25
+ tu4kai
26
+ w1zet09
27
+ antifrizovyi_rubb
28
+ vovchikgolita
29
+ mesgoredit
30
+ lexxxaxill
31
+ bytilka12
32
+ antosha_t2x
33
+ ostrovt2x2
34
+ otvertky_eee
35
+ t2xstream
36
+ t2x2_clips_
37
+ 89_sqd_fun
38
+ xray89sqd
39
+ kafix728
40
+ t2x2_momentus
41
+ hesus_twitchh
42
+ satoshitwitch
43
+ gven.h
44
+ nocaphistoryia
45
+ t2x2rezka89
46
+ godofnarezok
47
+ qst1mb
48
+ _t2x2_live_
Html/index.html ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Таймер</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
8
+ <style>
9
+ *{
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+ body{
15
+ font-family: "Roboto Mono", monospace;
16
+ }
17
+ #timerWrap {
18
+ position: relative;
19
+ display: flex;
20
+ flex-direction: column;
21
+ justify-content: center;
22
+ align-items: center;
23
+ padding: 10px;
24
+ margin-top: 20px;
25
+
26
+ }
27
+ #timer {
28
+
29
+
30
+
31
+ white-space-collapse: collapse;
32
+ display: flex;
33
+ font-size: 9vw;
34
+ justify-content: center;
35
+
36
+ font-weight: 900;
37
+ color: rgb(255, 255, 255);
38
+ text-shadow: 0px 1px 3px rgb(0, 0, 0),
39
+ 1px 2px 5px rgb(0, 0, 0),
40
+ 2px 4px 10px rgb(0, 0, 0),
41
+ 3px 6px 15px rgb(0, 0, 0)
42
+ ;
43
+ -webkit-text-stroke: 2px rgb(0, 0, 0);
44
+
45
+ }
46
+ #timer > div {
47
+ text-align: center;
48
+ transform: translateY(-3%);
49
+
50
+
51
+ }
52
+ #timer > div::after{
53
+ content: ":";
54
+ }
55
+ #timer > div:last-child::after{
56
+ content: "";
57
+ }
58
+
59
+ #labelTimer{
60
+ font-size: 36px;
61
+ font-weight: 600;
62
+ position: absolute;
63
+ bottom: 0px;
64
+ line-height: 0px;
65
+ color: #Fff;
66
+ -webkit-text-stroke: 2px rgb(0, 0, 0);
67
+ text-shadow: 0px 1px 3px rgb(0, 0, 0),
68
+ 1px 2px 5px rgb(0, 0, 0),
69
+ 2px 4px 10px rgb(0, 0, 0);
70
+ }
71
+ .timeToRestart__wrap{
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ margin-top: 32px;
76
+ gap: 16px;
77
+ }
78
+ </style>
79
+ </head>
80
+ <body>
81
+
82
+ <div id="timerWrap">
83
+ <p id="labelTimer">Осталось</p>
84
+ <div id="timer">
85
+ <div id="days">NO</div>
86
+ <div id="hours">CON</div>
87
+ <div id="minutes">WEB</div>
88
+ <div id="seconds">SOKET</div>
89
+ </div>
90
+
91
+ </div>
92
+ <div class="timeToRestart__wrap">
93
+
94
+ <div id="timeToRestart">
95
+ 1000
96
+ </div>
97
+
98
+ </div>
99
+
100
+
101
+ <script src="main.js"></script>
102
+
103
+ </body>
104
+ </html>
Html/main.js ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let days = document.getElementById("days");
2
+ let hours = document.getElementById("hours");
3
+ let minutes = document.getElementById("minutes");
4
+ let seconds = document.getElementById("seconds");
5
+
6
+ let interval = document.getElementById("timeToRestart");
7
+
8
+ function timeNotTwoDigits(number) {
9
+ if (number < 10 && number >= 0) {
10
+ return "0" + number;
11
+ }
12
+ if (number < 0&& number > -10) {
13
+ return "-0" + Math.abs(number);
14
+ }
15
+
16
+ return number;
17
+ }
18
+
19
+ const HEARTBEAT_INTERVAL = 1000; // Интервал heartbeat в миллисекундах
20
+ const HEARTBEAT_VALUE = 1; // Значение heartbeat-сообщения
21
+ const RECONNECT_INTERVAL = 1000; // Интервал переподключения в миллисекундах
22
+ const WS_URL = "ws://localhost:8001";
23
+
24
+ class WebSocketClient {
25
+ constructor(url, reconnectInterval, heartbeatInterval) {
26
+ this.url = url;
27
+ this.reconnectInterval = reconnectInterval;
28
+ this.heartbeatInterval = heartbeatInterval;
29
+ this.websocket = null;
30
+ this.isConnecting = false;
31
+ this.heartbeatTimer = null;
32
+ this.connect();
33
+ }
34
+
35
+ connect() {
36
+ if (this.isConnecting) {
37
+ console.log("Подключение уже в процессе.");
38
+ return;
39
+ }
40
+ this.isConnecting = true;
41
+ console.log("Попытка подключения к:", this.url);
42
+ this.websocket = new WebSocket(this.url);
43
+
44
+ this.websocket.onopen = () => {
45
+ console.log("Соединение WebSocket открыто");
46
+ this.isConnecting = false;
47
+ this.startHeartbeat();
48
+ this.onOpen();
49
+ };
50
+
51
+ this.websocket.onmessage = (event) => {
52
+ this.onMessage(event);
53
+ };
54
+
55
+ this.websocket.onclose = (event) => {
56
+ console.log("Соединение WebSocket закрыто:", event.code, event.reason);
57
+ this.isConnecting = false;
58
+ this.stopHeartbeat();
59
+ this.onClose(event);
60
+ if (event.code !== 1000) {
61
+ this.reconnect();
62
+ }
63
+ };
64
+
65
+ this.websocket.onerror = (error) => {
66
+ console.error("Ошибка WebSocket:", error);
67
+ this.isConnecting = false;
68
+ this.stopHeartbeat();
69
+ this.onError(error);
70
+ this.reconnect();
71
+ };
72
+ }
73
+
74
+ startHeartbeat() {
75
+ if (this.heartbeatInterval <= 0) return
76
+ this.heartbeatTimer = setInterval(() => {
77
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
78
+ this.websocket.send(HEARTBEAT_VALUE);
79
+ } else {
80
+ console.log("Не удалось отправить heartbeat, соединение не установлено");
81
+ this.stopHeartbeat();
82
+ }
83
+ }, this.heartbeatInterval);
84
+ }
85
+
86
+ stopHeartbeat() {
87
+ clearInterval(this.heartbeatTimer);
88
+ }
89
+
90
+
91
+ reconnect() {
92
+ console.log(`Попытка переподключения через ${this.reconnectInterval / 1000} секунд.`);
93
+ setTimeout(() => {
94
+ this.connect();
95
+ }, this.reconnectInterval);
96
+ }
97
+
98
+ send(data) {
99
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
100
+ this.websocket.send(data);
101
+ } else {
102
+ console.log("Соединение WebSocket не установлено, сообщение не отправлено:", data)
103
+ }
104
+ }
105
+
106
+ onOpen() {
107
+ console.log("Соединение открыто!");
108
+ }
109
+
110
+ onMessage(event) {
111
+ try {
112
+ let data = JSON.parse(event.data);
113
+ this.setTimerFromWSData(data.data.time);
114
+ this.setReloadTimeFromWSData(data.data.timerToRestart);
115
+ this.setTextUpdating(data.data.isUpdating);
116
+
117
+ } catch (e) {
118
+ console.log("Получено некорректное сообщение", event.data)
119
+ }
120
+ }
121
+
122
+ onClose(event) {
123
+ console.log("Соединение закрыто.", event)
124
+ }
125
+
126
+ onError(error) {
127
+ console.log("Произошла ошибка:", error)
128
+ }
129
+
130
+ setTimerFromWSData(data) {
131
+ let time = data;
132
+ let days_ = Math.floor(time / (60 * 60 * 24));
133
+ let hours_ = Math.floor(time / (60 * 60)) % 24;
134
+ let minutes_ = Math.floor(time / 60) % 60;
135
+ let seconds_ = time % 60;
136
+ days.innerHTML = timeNotTwoDigits(days_);
137
+ hours.innerHTML = timeNotTwoDigits(hours_);
138
+ minutes.innerHTML = timeNotTwoDigits(minutes_);
139
+ seconds.innerHTML = timeNotTwoDigits(seconds_);
140
+ }
141
+ setReloadTimeFromWSData(data) {
142
+ let time = data;
143
+ interval.innerHTML = `Обновление через - ${time} секунд`;
144
+ }
145
+ setTextUpdating(data) {
146
+ if (data) {
147
+
148
+ interval.innerHTML = "Обновление данных...";
149
+ }
150
+ }
151
+ }
152
+
153
+
154
+ const client = new WebSocketClient(WS_URL, RECONNECT_INTERVAL, HEARTBEAT_INTERVAL);
Html/reconnecting-websoket.min.js ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // MIT License:
2
+ //
3
+ // Copyright (c) 2010-2012, Joe Walnes
4
+ //
5
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ // of this software and associated documentation files (the "Software"), to deal
7
+ // in the Software without restriction, including without limitation the rights
8
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ // copies of the Software, and to permit persons to whom the Software is
10
+ // furnished to do so, subject to the following conditions:
11
+ //
12
+ // The above copyright notice and this permission notice shall be included in
13
+ // all copies or substantial portions of the Software.
14
+ //
15
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ // THE SOFTWARE.
22
+
23
+ /**
24
+ * This behaves like a WebSocket in every way, except if it fails to connect,
25
+ * or it gets disconnected, it will repeatedly poll until it successfully connects
26
+ * again.
27
+ *
28
+ * It is API compatible, so when you have:
29
+ * ws = new WebSocket('ws://....');
30
+ * you can replace with:
31
+ * ws = new ReconnectingWebSocket('ws://....');
32
+ *
33
+ * The event stream will typically look like:
34
+ * onconnecting
35
+ * onopen
36
+ * onmessage
37
+ * onmessage
38
+ * onclose // lost connection
39
+ * onconnecting
40
+ * onopen // sometime later...
41
+ * onmessage
42
+ * onmessage
43
+ * etc...
44
+ *
45
+ * It is API compatible with the standard WebSocket API, apart from the following members:
46
+ *
47
+ * - `bufferedAmount`
48
+ * - `extensions`
49
+ * - `binaryType`
50
+ *
51
+ * Latest version: https://github.com/joewalnes/reconnecting-websocket/
52
+ * - Joe Walnes
53
+ *
54
+ * Syntax
55
+ * ======
56
+ * var socket = new ReconnectingWebSocket(url, protocols, options);
57
+ *
58
+ * Parameters
59
+ * ==========
60
+ * url - The url you are connecting to.
61
+ * protocols - Optional string or array of protocols.
62
+ * options - See below
63
+ *
64
+ * Options
65
+ * =======
66
+ * Options can either be passed upon instantiation or set after instantiation:
67
+ *
68
+ * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 });
69
+ *
70
+ * or
71
+ *
72
+ * var socket = new ReconnectingWebSocket(url);
73
+ * socket.debug = true;
74
+ * socket.reconnectInterval = 4000;
75
+ *
76
+ * debug
77
+ * - Whether this instance should log debug messages. Accepts true or false. Default: false.
78
+ *
79
+ * automaticOpen
80
+ * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close().
81
+ *
82
+ * reconnectInterval
83
+ * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000.
84
+ *
85
+ * maxReconnectInterval
86
+ * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000.
87
+ *
88
+ * reconnectDecay
89
+ * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5.
90
+ *
91
+ * timeoutInterval
92
+ * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000.
93
+ *
94
+ */
95
+ (function (global, factory) {
96
+ if (typeof define === 'function' && define.amd) {
97
+ define([], factory);
98
+ } else if (typeof module !== 'undefined' && module.exports){
99
+ module.exports = factory();
100
+ } else {
101
+ global.ReconnectingWebSocket = factory();
102
+ }
103
+ })(this, function () {
104
+
105
+ if (!('WebSocket' in window)) {
106
+ return;
107
+ }
108
+
109
+ function ReconnectingWebSocket(url, protocols, options) {
110
+
111
+ // Default settings
112
+ var settings = {
113
+
114
+ /** Whether this instance should log debug messages. */
115
+ debug: false,
116
+
117
+ /** Whether or not the websocket should attempt to connect immediately upon instantiation. */
118
+ automaticOpen: true,
119
+
120
+ /** The number of milliseconds to delay before attempting to reconnect. */
121
+ reconnectInterval: 1000,
122
+ /** The maximum number of milliseconds to delay a reconnection attempt. */
123
+ maxReconnectInterval: 30000,
124
+ /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
125
+ reconnectDecay: 1.5,
126
+
127
+ /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
128
+ timeoutInterval: 2000,
129
+
130
+ /** The maximum number of reconnection attempts to make. Unlimited if null. */
131
+ maxReconnectAttempts: null,
132
+
133
+ /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
134
+ binaryType: 'blob'
135
+ }
136
+ if (!options) { options = {}; }
137
+
138
+ // Overwrite and define settings with options if they exist.
139
+ for (var key in settings) {
140
+ if (typeof options[key] !== 'undefined') {
141
+ this[key] = options[key];
142
+ } else {
143
+ this[key] = settings[key];
144
+ }
145
+ }
146
+
147
+ // These should be treated as read-only properties
148
+
149
+ /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */
150
+ this.url = url;
151
+
152
+ /** The number of attempted reconnects since starting, or the last successful connection. Read only. */
153
+ this.reconnectAttempts = 0;
154
+
155
+ /**
156
+ * The current state of the connection.
157
+ * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
158
+ * Read only.
159
+ */
160
+ this.readyState = WebSocket.CONNECTING;
161
+
162
+ /**
163
+ * A string indicating the name of the sub-protocol the server selected; this will be one of
164
+ * the strings specified in the protocols parameter when creating the WebSocket object.
165
+ * Read only.
166
+ */
167
+ this.protocol = null;
168
+
169
+ // Private state variables
170
+
171
+ var self = this;
172
+ var ws;
173
+ var forcedClose = false;
174
+ var timedOut = false;
175
+ var eventTarget = document.createElement('div');
176
+
177
+ // Wire up "on*" properties as event handlers
178
+
179
+ eventTarget.addEventListener('open', function(event) { self.onopen(event); });
180
+ eventTarget.addEventListener('close', function(event) { self.onclose(event); });
181
+ eventTarget.addEventListener('connecting', function(event) { self.onconnecting(event); });
182
+ eventTarget.addEventListener('message', function(event) { self.onmessage(event); });
183
+ eventTarget.addEventListener('error', function(event) { self.onerror(event); });
184
+
185
+ // Expose the API required by EventTarget
186
+
187
+ this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
188
+ this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
189
+ this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
190
+
191
+ /**
192
+ * This function generates an event that is compatible with standard
193
+ * compliant browsers and IE9 - IE11
194
+ *
195
+ * This will prevent the error:
196
+ * Object doesn't support this action
197
+ *
198
+ * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563
199
+ * @param s String The name that the event should use
200
+ * @param args Object an optional object that the event will use
201
+ */
202
+ function generateEvent(s, args) {
203
+ var evt = document.createEvent("CustomEvent");
204
+ evt.initCustomEvent(s, false, false, args);
205
+ return evt;
206
+ };
207
+
208
+ this.open = function (reconnectAttempt) {
209
+ ws = new WebSocket(self.url, protocols || []);
210
+ ws.binaryType = this.binaryType;
211
+
212
+ if (reconnectAttempt) {
213
+ if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
214
+ return;
215
+ }
216
+ } else {
217
+ eventTarget.dispatchEvent(generateEvent('connecting'));
218
+ this.reconnectAttempts = 0;
219
+ }
220
+
221
+ if (self.debug || ReconnectingWebSocket.debugAll) {
222
+ console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
223
+ }
224
+
225
+ var localWs = ws;
226
+ var timeout = setTimeout(function() {
227
+ if (self.debug || ReconnectingWebSocket.debugAll) {
228
+ console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
229
+ }
230
+ timedOut = true;
231
+ localWs.close();
232
+ timedOut = false;
233
+ }, self.timeoutInterval);
234
+
235
+ ws.onopen = function(event) {
236
+ clearTimeout(timeout);
237
+ if (self.debug || ReconnectingWebSocket.debugAll) {
238
+ console.debug('ReconnectingWebSocket', 'onopen', self.url);
239
+ }
240
+ self.protocol = ws.protocol;
241
+ self.readyState = WebSocket.OPEN;
242
+ self.reconnectAttempts = 0;
243
+ var e = generateEvent('open');
244
+ e.isReconnect = reconnectAttempt;
245
+ reconnectAttempt = false;
246
+ eventTarget.dispatchEvent(e);
247
+ };
248
+
249
+ ws.onclose = function(event) {
250
+ clearTimeout(timeout);
251
+ ws = null;
252
+ if (forcedClose) {
253
+ self.readyState = WebSocket.CLOSED;
254
+ eventTarget.dispatchEvent(generateEvent('close'));
255
+ } else {
256
+ self.readyState = WebSocket.CONNECTING;
257
+ var e = generateEvent('connecting');
258
+ e.code = event.code;
259
+ e.reason = event.reason;
260
+ e.wasClean = event.wasClean;
261
+ eventTarget.dispatchEvent(e);
262
+ if (!reconnectAttempt && !timedOut) {
263
+ if (self.debug || ReconnectingWebSocket.debugAll) {
264
+ console.debug('ReconnectingWebSocket', 'onclose', self.url);
265
+ }
266
+ eventTarget.dispatchEvent(generateEvent('close'));
267
+ }
268
+
269
+ var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
270
+ setTimeout(function() {
271
+ self.reconnectAttempts++;
272
+ self.open(true);
273
+ }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
274
+ }
275
+ };
276
+ ws.onmessage = function(event) {
277
+ if (self.debug || ReconnectingWebSocket.debugAll) {
278
+ console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
279
+ }
280
+ var e = generateEvent('message');
281
+ e.data = event.data;
282
+ eventTarget.dispatchEvent(e);
283
+ };
284
+ ws.onerror = function(event) {
285
+ if (self.debug || ReconnectingWebSocket.debugAll) {
286
+ console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
287
+ }
288
+ eventTarget.dispatchEvent(generateEvent('error'));
289
+ };
290
+ }
291
+
292
+ // Whether or not to create a websocket upon instantiation
293
+ if (this.automaticOpen == true) {
294
+ this.open(false);
295
+ }
296
+
297
+ /**
298
+ * Transmits data to the server over the WebSocket connection.
299
+ *
300
+ * @param data a text string, ArrayBuffer or Blob to send to the server.
301
+ */
302
+ this.send = function(data) {
303
+ if (ws) {
304
+ if (self.debug || ReconnectingWebSocket.debugAll) {
305
+ console.debug('ReconnectingWebSocket', 'send', self.url, data);
306
+ }
307
+ return ws.send(data);
308
+ } else {
309
+ throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
310
+ }
311
+ };
312
+
313
+ /**
314
+ * Closes the WebSocket connection or connection attempt, if any.
315
+ * If the connection is already CLOSED, this method does nothing.
316
+ */
317
+ this.close = function(code, reason) {
318
+ // Default CLOSE_NORMAL code
319
+ if (typeof code == 'undefined') {
320
+ code = 1000;
321
+ }
322
+ forcedClose = true;
323
+ if (ws) {
324
+ ws.close(code, reason);
325
+ }
326
+ };
327
+
328
+ /**
329
+ * Additional public API method to refresh the connection if still open (close, re-open).
330
+ * For example, if the app suspects bad data / missed heart beats, it can try to refresh.
331
+ */
332
+ this.refresh = function() {
333
+ if (ws) {
334
+ ws.close();
335
+ }
336
+ };
337
+ }
338
+
339
+ /**
340
+ * An event listener to be called when the WebSocket connection's readyState changes to OPEN;
341
+ * this indicates that the connection is ready to send and receive data.
342
+ */
343
+ ReconnectingWebSocket.prototype.onopen = function(event) {};
344
+ /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */
345
+ ReconnectingWebSocket.prototype.onclose = function(event) {};
346
+ /** An event listener to be called when a connection begins being attempted. */
347
+ ReconnectingWebSocket.prototype.onconnecting = function(event) {};
348
+ /** An event listener to be called when a message is received from the server. */
349
+ ReconnectingWebSocket.prototype.onmessage = function(event) {};
350
+ /** An event listener to be called when an error occurs. */
351
+ ReconnectingWebSocket.prototype.onerror = function(event) {};
352
+
353
+ /**
354
+ * Whether all instances of ReconnectingWebSocket should log debug messages.
355
+ * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true.
356
+ */
357
+ ReconnectingWebSocket.debugAll = false;
358
+
359
+ ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
360
+ ReconnectingWebSocket.OPEN = WebSocket.OPEN;
361
+ ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
362
+ ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
363
+
364
+ return ReconnectingWebSocket;
365
+ });
Main.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import websockets
3
+ import time
4
+ import json
5
+ import threading
6
+ import requests
7
+ import datetime
8
+ import traceback
9
+ from playwright.async_api import async_playwright
10
+ from TikTok.Server.main import getInfo
11
+ from TikTok.Server.SaveTotalView import saveTotalViewAndVideos, getTotalDict
12
+ from TikTok.Cookies.cookie import get_tiktok_cookies_from_file
13
+ import os
14
+ import random
15
+ import math
16
+ # Replace with your actual function to get TikTok data
17
+
18
+
19
+ def get_tiktok_data(hashtag="костиккакто", userlistLink="Data/TXT/Cacto0o.txt") -> dict:
20
+ try:
21
+ return getInfo(hashtag, userlistLink)
22
+ except requests.exceptions.RequestException as e:
23
+ print(f"Error fetching TikTok data: {e}")
24
+ return None
25
+ except Exception as e:
26
+ print(f"An unexpected error occurred: {e}")
27
+ return None
28
+
29
+
30
+ # Global variables (better to use a class)
31
+ startTime = 1734648586
32
+ donateAddTime = 35497352
33
+ endTime = startTime + donateAddTime
34
+ data_dict = None
35
+ global lastReloadTime
36
+ global doUpdating
37
+ lastReloadTime = time.time()
38
+
39
+
40
+ def save_data(data):
41
+ if not os.path.exists("Data/JSON/"):
42
+ os.makedirs("Data/JSON/")
43
+ if type(data) == str:
44
+ json_acceptable_string = data.replace("'", "\"")
45
+ data = json.loads(json_acceptable_string)
46
+
47
+ with open("Data/JSON/data.json", "r") as f:
48
+ data_dict = json.loads(f.read())
49
+ for user_data in data["userStats"]:
50
+ if user_data == 0:
51
+ continue
52
+ for new_user_data in data_dict["userStats"]:
53
+ if new_user_data == 0:
54
+ continue
55
+ if (user_data["username"] == new_user_data["username"]):
56
+ user_data["total_views"] = new_user_data["total_views"]
57
+ user_data["total_videos_with_tag"] = new_user_data["total_videos_with_tag"]
58
+ print(f"Updated user: {user_data['username']}")
59
+ break
60
+ else:
61
+
62
+ data_dict.get("userStats").append(user_data)
63
+ print(f"newUser {user_data['username']}")
64
+
65
+ with open("Data/JSON/dataNew.json", "w") as f:
66
+ f.write(json.dumps(data_dict))
67
+
68
+
69
+ def open_dataDict() -> dict:
70
+ with open("Data/JSON/TotalView.json", "r") as f:
71
+ data = f.read()
72
+ return json.loads(data)
73
+
74
+
75
+ async def send_data_to_websocket(websocket):
76
+ global data_dict
77
+ global lastReloadTime
78
+ while True:
79
+ data_dict = open_dataDict()
80
+ if data_dict is not None:
81
+ data_dict_a: dict = data_dict
82
+ tiktokTime = startTime + data_dict_a.get('total_total_views', 0)
83
+ time_left = int(tiktokTime - time.time())
84
+ timeToRestart = (lastReloadTime + 300) - time.time()
85
+ transferData = json.dumps({"type": "transfer", "data": {
86
+ "time": time_left, "timerToRestart": timeToRestart}})
87
+
88
+ try:
89
+ await websocket.send(transferData)
90
+ except websockets.exceptions.ConnectionClosedError:
91
+ print("Websocket connection closed. Exiting send thread.")
92
+ break
93
+ await asyncio.sleep(1)
94
+
95
+
96
+ def fetch_tiktok_data_periodically_main(hashtag="костиккакто"):
97
+ asyncio.run(fetch_tiktok_data_periodically(hashtag))
98
+
99
+
100
+ # 5 minutes
101
+ async def fetch_tiktok_data_periodically(hashtag="костиккакто", interval=300):
102
+ global data_dict
103
+ global lastReloadTime
104
+ global doUpdating
105
+ isFirst = True
106
+ while True:
107
+ # print("Starting fetch_tiktok_data_periodically")
108
+ # if isFirst:
109
+
110
+ # isFirst = False
111
+ # data = getTotalDict()
112
+ # print(data)
113
+ # else:
114
+
115
+ doUpdating = True
116
+ data: dict = await get_tiktok_data(hashtag, userlistLink="Data/TXT/Cacto0o.txt")
117
+ saveTotalViewAndVideos(hashtag)
118
+ data_dict = open_dataDict()
119
+ print(data_dict)
120
+
121
+ # if data.get('total_total_views', 0) > 0:
122
+ # save_data(data)
123
+ doUpdating = False
124
+ lastReloadTime = time.time()
125
+ time.sleep(interval)
126
+
127
+
128
+ def update_data_periodically():
129
+ global data_dict
130
+ print("Starting update_data_periodically")
131
+ hashtag = "костиккакто"
132
+ while True:
133
+ #
134
+ saveTotalViewAndVideos(hashtag)
135
+ data = open_dataDict()
136
+ if data.get('total_views', 0) > 0:
137
+ data_dict = open_dataDict()
138
+ time.sleep(1)
139
+
140
+
141
+ async def handler(websocket):
142
+ global data_dict
143
+ global doUpdating
144
+ while True:
145
+ try:
146
+ data_dict = open_dataDict()
147
+ # Slight delay to avoid immediate re-execution
148
+ if data_dict is not None:
149
+ tiktokTime = startTime + \
150
+ math.floor(data_dict.get('total_views', 0) / 30000 * 3600)
151
+ time_left = int(tiktokTime - time.time())
152
+ timeToRestart = int((lastReloadTime + 300) - time.time())
153
+ transferData = json.dumps({"type": "transfer", "data": {"time": time_left,
154
+ "timerToRestart": timeToRestart,
155
+ "isUpdating": doUpdating
156
+ }})
157
+ await websocket.send(transferData)
158
+ await asyncio.sleep(1)
159
+ except websockets.exceptions.ConnectionClosedError:
160
+ print("Websocket connection closed.")
161
+ break
162
+ except Exception as e:
163
+ print(f"Error in handler: {e}")
164
+ break
165
+
166
+
167
+ def msTokenFromTiktok():
168
+ asyncio.run(msTokenFromTiktokMain())
169
+
170
+
171
+ async def msTokenFromTiktokMain():
172
+ playwright = await async_playwright().start()
173
+ browser = await playwright.chromium.launch(
174
+ headless=False,
175
+ executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe"
176
+ )
177
+ page = await browser.new_page()
178
+ await page.goto("https://www.tiktok.com/")
179
+ try:
180
+ await asyncio.sleep(2)
181
+ await page.goto("https://www.tiktok.com/")
182
+ while True:
183
+ await asyncio.sleep(random.uniform(0, 2))
184
+ random_number = random.randint(1, 1000)
185
+ if random_number % 2 == 0:
186
+ await page.keyboard.press("L")
187
+ await page.keyboard.press("ArrowDown")
188
+ await asyncio.sleep(random.uniform(0, 2))
189
+ cookies = await page.context.cookies()
190
+ # Save cookies to a file
191
+ with open("Data/JSON/cookies.json", "w") as f:
192
+ json.dump(cookies, f)
193
+ print(get_tiktok_cookies_from_file("Data/JSON/cookies.json"))
194
+ await asyncio.sleep(10)
195
+ except Exception as e:
196
+ print(f"An error occurred: {e}")
197
+
198
+ await browser.close()
199
+
200
+
201
+ async def main():
202
+ async with websockets.serve(handler, "localhost", 8001):
203
+ print("Server started on ws://localhost:8001")
204
+
205
+ # Start separate thread for fetching data
206
+ threadTikTokInfo = threading.Thread(
207
+ target=fetch_tiktok_data_periodically_main)
208
+ threadTikTokInfo.daemon = True # Allow the main thread to exit
209
+ threadTikTokInfo.start()
210
+
211
+ # threadGettingMsToken = threading.Thread(target=msTokenFromTiktok)
212
+ # threadGettingMsToken.daemon = True # Allow the main thread to exit
213
+ # threadGettingMsToken.start()
214
+
215
+ threadUpdate = threading.Thread(target=update_data_periodically)
216
+ threadUpdate.daemon = True # Allow the main thread to exit
217
+ threadUpdate.start()
218
+
219
+ await asyncio.Future() # Keep the event loop running
220
+
221
+ if __name__ == "__main__":
222
+ asyncio.run(main())
README.md CHANGED
@@ -1,12 +1,6 @@
1
  ---
2
  title: TikTokOpen
3
- emoji:
4
- colorFrom: green
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 5.9.1
8
- app_file: app.py
9
- pinned: false
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: TikTokOpen
3
+ app_file: gradioa.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.9.1
 
 
6
  ---
 
 
TikTok/Cookies/__init__.py ADDED
File without changes
TikTok/Cookies/cookie.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import browser_cookie3
2
+ import json
3
+
4
+ def getMsToken():
5
+ cookie_keys = ["msToken"]
6
+ json_cookies = get_tiktok_cookies(cookie_keys)
7
+ if json_cookies["found"]:
8
+ ms_token = json_cookies["cookies"]["msToken"]
9
+ #print(ms_token)
10
+ else:
11
+ raise Exception("Missing cookie msToken. Login to your tiktok account and retry")
12
+ saveMsToken(ms_token)
13
+ return ms_token
14
+
15
+ def saveMsToken(ms_token):
16
+ with open("Data/TXT/Data/ms_token.txt", "w") as f:
17
+ f.write(ms_token)
18
+ def readOldMsToken():
19
+ with open("Data/TXT/Data/ms_token.txt", "r") as f:
20
+ ms_token = f.read()
21
+ return ms_token
22
+
23
+ def get_tiktok_cookies(cookie_keys):
24
+ # Try to get cookie from browser
25
+ ref = ["chromium", "opera", "edge", "firefox", "chrome", "brave"]
26
+ index = 0
27
+ json_cookie = {}
28
+ found = False
29
+ for cookie_fn in [
30
+
31
+ browser_cookie3.firefox,
32
+ browser_cookie3.chrome,
33
+ browser_cookie3.brave,
34
+ ]:
35
+ try:
36
+ for cookie in cookie_fn(domain_name="tiktok.com"):
37
+
38
+ if ('tiktok.com' in cookie.domain):
39
+
40
+ # print(f"COOKIE - {ref[index]}: {cookie}")
41
+ if (cookie.name in cookie_keys):
42
+ json_cookie['browser'] = ref[index]
43
+ json_cookie[cookie.name] = cookie.value
44
+ json_cookie[cookie.name + '_expires'] = cookie.expires
45
+
46
+ # Check
47
+ found = True
48
+ for key in cookie_keys:
49
+ if (json_cookie.get(key, "") == ""):
50
+ found = False
51
+ break
52
+
53
+ except Exception as e:
54
+ print(e)
55
+
56
+ index += 1
57
+
58
+ if (found):
59
+ break
60
+ #print("found " + str(found))
61
+ return {"found": found, "cookies": json_cookie}
62
+
63
+
64
+
65
+ def get_tiktok_cookies_from_file(filepath: str):
66
+ msToken = ""
67
+ cookies = {}
68
+ with open(filepath, "r") as f:
69
+ cookies = f.read()
70
+
71
+ cookies = json.loads(cookies)
72
+
73
+ for cookie in cookies:
74
+ cookie: dict
75
+ if cookie.get("name", "") == "msToken" and cookie.get("domain", "") == ".tiktok.com":
76
+ msToken = cookie.get("value", "")
77
+ break
78
+
79
+
80
+ if msToken is None:
81
+ raise Exception("Missing cookie msToken. Login to your tiktok account and retry")
82
+ saveMsToken(msToken)
83
+ return msToken
84
+
85
+ def getCookiesFromFile(filepath: str):
86
+ cookies = {}
87
+ with open(filepath, "r") as f:
88
+ cookies = f.read()
89
+
90
+ cookies = json.loads(cookies)
91
+ return cookies
92
+ if __name__ == "__main__":
93
+ print(get_tiktok_cookies_from_file())
94
+
TikTok/Server/SaveTotalView.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ def saveTotalViewAndVideos(hashtag: str):
5
+ allData = {}
6
+ hashtag = "костиккакто"
7
+ if not os.path.exists(f"Data/JSON/Users/{hashtag}"):
8
+ print('a')
9
+ os.makedirs(f"Data/JSON/Users/{hashtag}")
10
+
11
+ for user in os.listdir(f"Data/JSON/Users/{hashtag}"):
12
+ if user == "TotalView.json":
13
+ continue
14
+ with open(f"Data/JSON/Users/{hashtag}/{user}", "r") as f:
15
+ allData[user] = json.loads(f.read())
16
+ totalVideos = 0
17
+ totalViews = 0
18
+ for user in allData:
19
+ totalViews += allData[user]["total_views"]
20
+ totalVideos += allData[user]["total_videos_with_tag"]
21
+ dirname = "Data/JSON/TotalView.json"
22
+ if not os.path.exists(os.path.dirname(dirname)):
23
+ os.makedirs(os.path.dirname(dirname))
24
+
25
+ with open(f"Data/JSON/TotalView.json", "w") as f:
26
+ f.write(json.dumps({
27
+ "total_views": totalViews,
28
+ "total_videos_with_tag": totalVideos
29
+ }))
30
+
31
+ def getTotalDict() -> dict:
32
+ if os.path.exists(f"Data/JSON/TotalView.json"):
33
+ with open(f"Data/JSON/TotalView.json", "r") as f:
34
+ return json.loads(f.read())
35
+ else:
36
+ return {
37
+ "total_views": 0,
38
+ "total_videos_with_tag": 0
39
+ }
40
+
41
+
42
+ if __name__ == "__main__":
43
+ # load all json from Data/JSON/User/{hashtag}/*.json
44
+
45
+ # save all json to Data/JSON/User/{hashtag}/TotalView.json
46
+ saveTotalViewAndVideos("костиккакто")
47
+ print(getTotalDict())
48
+
49
+
50
+
51
+
52
+
TikTok/Server/__init__.py ADDED
File without changes
TikTok/Server/main.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from TikTok.Statistic.tiktok import tiktokUserCountVideoViews, SameMsTokenException
3
+ from TikTok.Statistic.SingleUser import users_videos_with_hashtag
4
+ from TikTok.Cookies.cookie import getMsToken, readOldMsToken, saveMsToken, get_tiktok_cookies_from_file,getCookiesFromFile
5
+ from TikTok.Server.users import get_user_list
6
+ import time
7
+ import os
8
+ import json
9
+
10
+ def getNewMsToken():
11
+ try:
12
+
13
+
14
+ ms_token = get_tiktok_cookies_from_file("cookies.txt")
15
+ print(f" ms_token:\t {ms_token} \n")
16
+
17
+
18
+ return ms_token
19
+
20
+ except Exception as e:
21
+ print("Exception" + e)
22
+ except SameMsTokenException as e:
23
+ print(e.message)
24
+ except ValueError as e:
25
+ print(e)
26
+ print("Please check your ms_token")
27
+
28
+
29
+ def getUserList(userlistLink: str):
30
+ userlist = get_user_list(userlistLink)
31
+ if not userlist:
32
+ raise Exception("No users found in the user list.")
33
+ return userlist
34
+
35
+ async def divide_list(userlist: list, num_parts: int, selectedPart: int) -> list:
36
+ userlist = userlist[selectedPart::num_parts]
37
+ return userlist
38
+
39
+ def saveIndex(index: dict):
40
+ with open("Data/JSON/index.json", "w") as f:
41
+ json.dump(index, f)
42
+ def openIndex() -> tuple:
43
+ with open("Data/JSON/index.json", "r") as f:
44
+ index = f.read()
45
+ index = json.loads(index)
46
+
47
+ return index["parts"], index["selectedPart"]
48
+
49
+ async def getInfo(hashtag: str, userlistLink: str) -> dict:
50
+
51
+ # ms_token = get_tiktok_cookies_from_file("Data/JSON/cookies.json")
52
+ userlist = getUserList(userlistLink)
53
+ {
54
+ # length = len(userlist)
55
+ # try:
56
+ # num_parts, selectedPart = openIndex()
57
+ # print(f"num_parts: {num_parts}, selectedPart: {selectedPart}")
58
+ # except:
59
+ # print("No index.json")
60
+ # num_parts = 1
61
+ # selectedPart = 0
62
+
63
+ # if selectedPart >= num_parts -1:
64
+ # selectedPart = 0
65
+ # else:
66
+ # selectedPart += 1
67
+
68
+ # maxusersinrow = 16
69
+ # num_parts = length // maxusersinrow
70
+
71
+ # if num_parts == 0:
72
+ # num_parts = 1
73
+ # print(f"num_parts: {num_parts}, selectedPart: {selectedPart}")
74
+
75
+ # userSmallLists = await divide_list(userlist, num_parts, selectedPart)
76
+ # print(f"userSmallLists: {userSmallLists}")
77
+ # userlist = userSmallLists
78
+
79
+ # saveIndex({"parts": num_parts, "selectedPart": selectedPart})
80
+ }
81
+ blackList=getBlackList("Data/JSON/blackList.json")
82
+
83
+ result = await users_videos_with_hashtag(
84
+ usernameList=userlist,
85
+ hashtag=hashtag,
86
+ blackList=blackList
87
+ )
88
+
89
+
90
+ return result #result
91
+
92
+ def getBlackList(blackListFile: str) -> dict:
93
+ try:
94
+ with open(blackListFile, "r") as f:
95
+ blackList = f.read()
96
+ if not blackList:
97
+ return {}
98
+ json_blackList = json.loads(blackList)
99
+ return json_blackList
100
+ except Exception as e:
101
+ print(e)
102
+ return {}
103
+ #if __name__ == "__main__":
104
+ # ms_token= get_tiktok_cookies_from_file("cookies.txt")
105
+ # userlistLink = "tiktok_stats/tiktokNames.txt"
106
+ # userlistLink = "tiktok_stats/names.txt"
107
+ # userlist = getUserList(userlistLink)
108
+ # hashtag = "костиккакто"
109
+ # result = 0
110
+ # blackList=getBlackList("blackList.json")
111
+ # print(f"userlist = {blackList}, users = {blackList.get('usernames')}, videos = {blackList.get('videos')}")
112
+
113
+ # try:
114
+ # result = asyncio.run(tiktokUserCountVideoViews(
115
+ # userlist=userlist,
116
+ # ms_token="pLSi7qEbF7imuiF0_ySIDEJe_Ew97wEpGvTZL5Icr8WmcazmH8qwiGigUt7HwWbk6sNffDl6KqnK5Ll1WfqRawl3f-zVNtcSD6iAfRL86GzR5z2A7k5O1BrGtsumNbKFy2XuzYca1SAotXiHd16_",
117
+ # hashtag=hashtag,
118
+ # blackList=blackList
119
+ # ))
120
+ # except SameMsTokenException as e:
121
+ # print(e.message)
122
+
123
+ # print(f"returnValue = {result}")
124
+ #asyncio.run(getInfo("костиккакто", "tiktok_stats/tiktokNames.txt"))
TikTok/Server/users.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+
2
+ def get_user_list(file_path):
3
+ with open(file_path, 'r') as file:
4
+ user_list = [line.strip() for line in file]
5
+ return user_list
TikTok/Statistic/AsyncUser.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ..TikTokApi import TikTokApi
2
+
3
+ from ..TikTokApi.api.user import User
4
+ from ..TikTokApi.api.video import Video
5
+ import asyncio
6
+ import os
7
+ import json
8
+ from datetime import datetime
9
+ import math
10
+ ms_token = os.environ.get("ms_token", None) # get your own ms_token from your cookies on tiktok.com
11
+ maxvalue = 20
12
+ nowProcess = 0
13
+ def debug(debug: bool = False):
14
+ if debug:
15
+ os.environ["DEBUG"] = "True"
16
+ else:
17
+ os.environ["DEBUG"] = "False"
18
+
19
+ def openJson(path):
20
+ try:
21
+ with open(path, "r") as f:
22
+ return json.loads(f.read())
23
+ except:
24
+ raise Exception("Error opening json file")
25
+
26
+ def saveJson(path, data):
27
+ if not os.path.exists(os.path.dirname(path)):
28
+ os.makedirs(os.path.dirname(path))
29
+ with open(path, "w") as f:
30
+ f.write(json.dumps(data))
31
+
32
+ def openTxt(path):
33
+ try:
34
+ with open(path, "r") as f:
35
+ return f.read().splitlines()
36
+ except:
37
+ raise Exception("Error opening txt file")
38
+ with open(path, "r") as f:
39
+ return f.read().splitlines()
40
+
41
+ def saveTxt(path, data):
42
+ if not os.path.exists(os.path.dirname(path)):
43
+ os.makedirs(os.path.dirname(path))
44
+ with open(path, "w") as f:
45
+ f.write("\n".join(data))
46
+
47
+ def saveUserInfoInJson(username, data, hashtag = "default"):
48
+ saveJson(f"Data/JSON/Users/{hashtag}/{username}.json", data)
49
+
50
+
51
+ def debugPrint(text):
52
+
53
+ print(f"{datetime.now().strftime('%H:%M:%S.%f')}\t{text}")
54
+
55
+
56
+
57
+
58
+ async def users_videos_with_hashtag(usernameList, hashtag, blackList: dict[list] = None, ms_token: str = None):
59
+ '''
60
+ Asynchronous function that retrieves TikTok videos with a specific hashtag for a list of usernames, and saves the user's total views and total videos with the hashtag to a JSON file.
61
+
62
+ Parameters:
63
+ - `usernameList`: List of TikTok usernames to retrieve videos for.
64
+ - `hashtag`: Hashtag to search for in the user's videos.
65
+ - `blackList`: (Optional) Dictionary containing lists of usernames and video IDs to skip.
66
+ - `ms_token`: (Optional) TikTok API access token.
67
+
68
+ '''
69
+ async with TikTokApi() as api:
70
+ debugPrint("Creating sessions")
71
+
72
+ await api.create_sessions(ms_tokens=[ms_token],
73
+ num_sessions=1,
74
+ sleep_after=20,
75
+ headless=False,
76
+ executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe",
77
+ #browser="firefox",
78
+ override_browser_args=["--disable-blink-features=AutomationControlled"],
79
+
80
+ #starting_url="https://anycoindirect.eu"
81
+ )
82
+
83
+ tasks = [process_user(username=userName, api=api, hashtag=hashtag, blackList=blackList) for userName in usernameList]
84
+
85
+ debugPrint("Sessions created")
86
+ print(blackList.get("usernames", ""))
87
+ await asyncio.gather(*tasks)
88
+ {
89
+ # async for username in usernameList:
90
+ # if username in blackList.get("usernames", ""):
91
+ # debugPrint(f"Skipping user {username} because it is in the blacklist")
92
+ # continue
93
+ # debugPrint(f"Getting user {username}")
94
+ # debugPrint(f"username = {username}")
95
+ #
96
+ # try:
97
+ #
98
+ # user: User = api.user(username=username)
99
+ # user_data = await user.info()
100
+ # except:
101
+ # print(f"Error getting user {username}")
102
+ # continue
103
+ #
104
+ # videosLen = user_data["userInfo"]["stats"]["videoCount"]
105
+ #
106
+ # debugPrint(f"videosLen = {videosLen}")
107
+ # total_views = 0
108
+ # total_videos_with_tag = 0
109
+ #
110
+ # async for video in user.videos(count= videosLen):
111
+ # if video.id in blackList.get("videos", []):
112
+ # continue
113
+ # video: Video
114
+ # play_count = int(video.stats.get("playCount", 0))
115
+ # if any(str(h.name).lower() == hashtag for h in video.hashtags):
116
+ # total_views += play_count
117
+ # total_videos_with_tag += 1
118
+ #
119
+ # saveUserInfoInJson(username=username,
120
+ # data={
121
+ # "username": username,
122
+ # "total_views": total_views,
123
+ # "total_videos_with_tag": total_videos_with_tag},
124
+ # hashtag=hashtag)
125
+ # await asyncio.sleep(1)
126
+ #
127
+ #
128
+ }
129
+ await api.close_sessions()
130
+ await api.stop_playwright()
131
+
132
+ async def process_user(username, api, hashtag, blackList):
133
+ try:
134
+ if username in blackList.get("usernames", ""):
135
+ debugPrint(f"Skipping user {username} because it is in the blacklist")
136
+ return
137
+ debugPrint(f"Getting user {username}")
138
+ debugPrint(f"username = {username}")
139
+
140
+ try:
141
+
142
+ user: User = api.user(username=username)
143
+ user_data = await user.info()
144
+ except:
145
+ print(f"Error getting user {username}")
146
+ return
147
+ while nowProcess >= maxvalue:
148
+ debugPrint(f"Waiting for {username}")
149
+ await asyncio.sleep(1)
150
+ nowProcess += 1
151
+ videosLen = user_data["userInfo"]["stats"]["videoCount"]
152
+
153
+ debugPrint(f"videosLen = {videosLen}")
154
+ total_views = 0
155
+ total_videos_with_tag = 0
156
+
157
+ async for video in user.videos(count= videosLen):
158
+ if video.id in blackList.get("videos", []):
159
+ continue
160
+ video: Video
161
+ play_count = int(video.stats.get("playCount", 0))
162
+ if any(str(h.name).lower() == hashtag for h in video.hashtags):
163
+ total_views += play_count
164
+ total_videos_with_tag += 1
165
+ debugPrint(f"save {username} {total_views}")
166
+ saveUserInfoInJson(username=username,
167
+ data={
168
+ "username": username,
169
+ "total_views": total_views,
170
+ "total_videos_with_tag": total_videos_with_tag},
171
+ hashtag=hashtag)
172
+ except:
173
+ nowProcess -= 1
174
+ print(f"Error getting user {username} !")
175
+ return
176
+
177
+ if __name__ == "__main__":
178
+ os.environ["DEBUG"] = "True"
179
+ #print(os.environ.pop("DEBUG", False))
180
+ usernameList = openTxt("Data/TXT/cacto0o.txt")
181
+ hashtag = "костиккакто"
182
+ blackList = openJson("Data/JSON/blackList.json")
183
+ asyncio.run(users_videos_with_hashtag(usernameList=usernameList, hashtag=hashtag, blackList=blackList))
TikTok/Statistic/SingleUser.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ..TikTokApi import TikTokApi
2
+
3
+ from ..TikTokApi.api.user import User
4
+ from ..TikTokApi.api.video import Video
5
+ import asyncio
6
+ import os
7
+ import json
8
+ from datetime import datetime
9
+ import math
10
+ import random
11
+ from tqdm import tqdm
12
+ # get your own ms_token from your cookies on tiktok.com
13
+ ms_token = os.environ.get("ms_token", None)
14
+
15
+
16
+ def debug(debug: bool = False):
17
+ if debug:
18
+ os.environ["DEBUG"] = "True"
19
+ else:
20
+ os.environ["DEBUG"] = "False"
21
+
22
+
23
+ def openJson(path):
24
+ try:
25
+ with open(path, "r") as f:
26
+ return json.loads(f.read())
27
+ except:
28
+ raise Exception("Error opening json file")
29
+
30
+
31
+ def saveJson(path, data):
32
+
33
+ if not os.path.exists(os.path.dirname(path)):
34
+ os.makedirs(os.path.dirname(path))
35
+ with open(path, "w") as f:
36
+ f.write(json.dumps(data))
37
+
38
+
39
+ def openTxt(path):
40
+ try:
41
+ with open(path, "r") as f:
42
+ return f.read().splitlines()
43
+ except:
44
+ raise Exception("Error opening txt file")
45
+
46
+
47
+
48
+ def saveTxt(path, data):
49
+ if not os.path.exists(os.path.dirname(path)):
50
+ os.makedirs(os.path.dirname(path))
51
+ with open(path, "w") as f:
52
+ f.write("\n".join(data))
53
+
54
+
55
+ def saveUserInfoInJson(username, data, hashtag="default"):
56
+ saveJson(f"Data/JSON/Users/{hashtag}/{username}.json", data)
57
+
58
+
59
+ def openUserInfoInJson(username, hashtag="default"):
60
+ try:
61
+ return openJson(f"Data/JSON/Users/{hashtag}/{username}.json")
62
+ except:
63
+ return None
64
+
65
+
66
+ def compareUserDataViewsAndSaveWithMore(user1, user2):
67
+ try:
68
+ if user1["total_views"] > user2["total_views"]:
69
+ return False
70
+ else:
71
+ return True
72
+ except:
73
+ print(f"Error comparing user data ")
74
+ return True
75
+
76
+
77
+ def debugPrint(text):
78
+ #print(f"{datetime.now().strftime('%H:%M:%S.%f')}\t{text}")
79
+ pass
80
+
81
+
82
+ async def users_videos_with_hashtag(usernameList, hashtag, blackList: dict[list] = None, ms_token: str = None):
83
+ '''
84
+ Asynchronous function that retrieves TikTok videos with a specific hashtag for a list of usernames, and saves the user's total views and total videos with the hashtag to a JSON file.
85
+
86
+ Parameters:
87
+ - `usernameList`: List of TikTok usernames to retrieve videos for.
88
+ - `hashtag`: Hashtag to search for in the user's videos.
89
+ - `blackList`: (Optional) Dictionary containing lists of usernames and video IDs to skip.
90
+ - `ms_token`: (Optional) TikTok API access token.
91
+
92
+ '''
93
+ async with TikTokApi() as api:
94
+ debugPrint("Creating sessions")
95
+ try:
96
+ cookieFormLast: list = [openJson("Data/JSON/cookies.json")]
97
+ except:
98
+ print("No cookies found, creating new sessions")
99
+ cookieFormLast = None
100
+
101
+ await api.create_sessions(ms_tokens=[ms_token],
102
+ num_sessions=1,
103
+ sleep_after=20,
104
+ headless=False,
105
+ executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe",
106
+ # browser="firefox",
107
+ override_browser_args=[
108
+ "--disable-blink-features=AutomationControlled"],
109
+ cookies=cookieFormLast,
110
+ starting_url="https://www.tiktok.com/@tiltocacto0o"
111
+ )
112
+
113
+ debugPrint("Sessions created")
114
+ print(blackList.get("usernames", ""))
115
+ for username in tqdm(usernameList):
116
+ if username in blackList.get("usernames", ""):
117
+ debugPrint(
118
+ f"Skipping user {username} because it is in the blacklist")
119
+ continue
120
+ debugPrint(f"Getting user {username}")
121
+ debugPrint(f"username = {username}")
122
+
123
+ try:
124
+
125
+ user: User = api.user(username=username)
126
+ user_data = await user.info()
127
+ except:
128
+ print(f"Error getting user {username}")
129
+ continue
130
+
131
+ videosLen = user_data["userInfo"]["stats"]["videoCount"]
132
+
133
+ debugPrint(f"videosLen = {videosLen} ")
134
+ total_views = 0
135
+ total_videos_with_tag = 0
136
+ try:
137
+ async for video in user.videos(count=videosLen):
138
+ if video.id in blackList.get("videos", []):
139
+ continue
140
+ video: Video
141
+
142
+ play_count = int(video.stats.get("playCount", 0))
143
+ if any(str(h.name).lower() == hashtag for h in video.hashtags):
144
+ total_views += play_count
145
+ total_videos_with_tag += 1
146
+ debugPrint(f"save {username} {total_views}")
147
+ openUserInfoInJson(username=username, hashtag=hashtag)
148
+ if compareUserDataViewsAndSaveWithMore(
149
+ openUserInfoInJson(username=username,
150
+ hashtag=hashtag),
151
+ {"username": username,
152
+ "total_views": total_views,
153
+ "total_videos_with_tag": total_videos_with_tag}
154
+ ):
155
+ saveUserInfoInJson(username=username,
156
+ data={
157
+ "username": username, "total_views": total_views, "total_videos_with_tag": total_videos_with_tag},
158
+ hashtag=hashtag)
159
+ else:
160
+ print(f"skip {username} {total_views}")
161
+ except Exception as e:
162
+ print(f"Error getting videos for user {username}")
163
+ print(e)
164
+ continue
165
+ await asyncio.sleep(random.uniform(0.5, 1.5))
166
+ debugPrint("Closing sessions")
167
+ cookietosave = await api.get_session_cookies(api.sessions[0])
168
+ saveJson("Data/JSON/cookies.json", cookietosave)
169
+ await api.close_sessions()
170
+ await api.stop_playwright()
171
+
172
+ if __name__ == "__main__":
173
+ os.environ["DEBUG"] = "True"
174
+ # print(os.environ.pop("DEBUG", False))
175
+ usernameList = openTxt("Data/TXT/cacto0o.txt")
176
+ hashtag = "костиккакто"
177
+ blackList = openJson("Data/JSON/blackList.json")
178
+ asyncio.run(users_videos_with_hashtag(
179
+ usernameList=usernameList, hashtag=hashtag, blackList=blackList))
TikTok/Statistic/__init__.py ADDED
File without changes
TikTok/Statistic/tiktok.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import time
3
+ import random
4
+ from ..TikTokApi import TikTokApi
5
+ from ..TikTokApi.exceptions import TikTokException
6
+
7
+
8
+
9
+ class SameMsTokenException(TikTokException):
10
+ """Raised when the same ms_token is used."""
11
+
12
+
13
+ def hashtagProcess(hashtag: str):
14
+ '''Converts the given hashtag string to lowercase.
15
+
16
+ Parameters:
17
+ - `hashtag: str`: The hashtag string to be processed.
18
+
19
+ Returns:
20
+ - `str`: The lowercase version of the input hashtag.'''
21
+
22
+ hashtag = hashtag.lower()
23
+ return hashtag
24
+
25
+ async def tiktokUserCountVideoViews(proxylist: list = None, ms_token: str = None, userlist: list = None, hashtag: str = None, cookies: list[dict] = None, blackList: dict[list] = None) -> dict:
26
+ '''Asynchronous function that retrieves video view counts for a list of TikTok users, filtering by a specified hashtag and blacklist.
27
+
28
+ Parameters:
29
+ - `proxylist: list = None`: A list of proxy servers to use.
30
+ - `ms_token: str = None`: A required TikTok MS token.
31
+ - `userlist: list = None`: A list of TikTok usernames to process.
32
+ - `hashtag: str = None`: A hashtag to filter the videos by.
33
+ - `cookies: list[dict] = None`: A list of cookie dictionaries to use for the TikTok API sessions.
34
+ - `blackList: dict[list] = None`: A dictionary containing lists of blacklisted usernames and video IDs.
35
+
36
+ Returns:
37
+ - `dict`: A dictionary containing the user statistics and the total number of views across all users.'''
38
+
39
+
40
+ if not ms_token:
41
+ raise ValueError("A TikTok MS token is required.")
42
+
43
+
44
+ if not userlist:
45
+ raise ValueError("A list of users is required.")
46
+
47
+ if blackList == None:
48
+ blackList = []
49
+ print(f"username = {blackList}")
50
+ hashtag = hashtagProcess(hashtag)
51
+ print(hashtag)
52
+ for userName in userlist:
53
+ if userName in blackList:
54
+ userlist.remove(userName)
55
+
56
+ try:
57
+ async with TikTokApi() as api:
58
+
59
+ startTime = time.time()
60
+ await api.create_sessions(headless=False, ms_tokens=[ms_token], num_sessions=1, sleep_after=30, cookies=cookies)
61
+
62
+
63
+ #tasks = [process_user(userName=userName, api=api, hashtag=hashtag, videoBlacklist=blackList.get("videos"), userBlacklist=blackList.get("usernames")) for userName in userlist]
64
+ #results = await asyncio.gather(*tasks)
65
+ results = []
66
+ for userName in userlist:
67
+ print(f"Processing user: {userName}")
68
+
69
+ results.append(asyncio.gather(process_user(userName=userName,
70
+ api=api,
71
+ hashtag=hashtag,
72
+ videoBlacklist=blackList.get("videos"),
73
+ userBlacklist=blackList.get("usernames"))))
74
+ total_total_views = 0
75
+
76
+ for i in results:
77
+ if isinstance(i, dict):
78
+ total_total_views += i['total_views']
79
+ elif isinstance(i, int):
80
+ total_total_views += i
81
+
82
+ results_as_dict = {"userStats": results, "total_total_views": total_total_views}
83
+
84
+ await api.close_sessions()
85
+ endTime = time.time()
86
+
87
+ print(f"Total views: \033[32m{total_total_views}\033[0m = process time: \033[31m{round(endTime - startTime, 4)}\033[0m")
88
+ return results_as_dict
89
+
90
+
91
+
92
+ except Exception as e:
93
+ if "TimeoutError" in str(e):
94
+ print(f"Error: {e}")
95
+ await api.close_sessions()
96
+ return 0
97
+ else:
98
+ print(f"An error occurred: {type(e).__name__}: {e}")
99
+ await api.close_sessions()
100
+ return 0
101
+
102
+ async def process_user(userName, hashtag:str, api:TikTokApi, userBlacklist, videoBlacklist):
103
+ '''Asynchronously processes a user's TikTok account, retrieving video data and calculating the total views for videos with a specified hashtag.
104
+
105
+ Args:
106
+ - `userName (str)`: The username of the TikTok user to process.
107
+ - `hashtag (str)`: The hashtag to search for in the user's videos.
108
+ - `api (TikTokApi)`: The TikTokApi instance to use for making API requests.
109
+ - `userBlacklist (list)`: A list of usernames to exclude from processing.
110
+ - `videoBlacklist (list)`: A list of video IDs to exclude from processing.
111
+
112
+ Returns:
113
+ - `dict`: A dictionary containing the username, total views, and total videos with the specified hashtag.
114
+ '''
115
+ print(userName)
116
+ #TODO: if user in blacklist then return 0
117
+ #time.sleep(random.randint(1, 5)/10)
118
+ if userName in userBlacklist:
119
+ print(f"{userName} in blacklist")
120
+ return 0
121
+ # await asyncio.sleep(random.randint(1, 5) / 10)
122
+ startTime = time.time()
123
+ try:
124
+ user = api.user(username=userName)
125
+
126
+ user_data = await user.info()
127
+
128
+ if "userInfo" not in user_data or "stats" not in user_data["userInfo"] or "videoCount" not in user_data["userInfo"]["stats"]:
129
+ print(f"Error: Invalid user data format for {userName}")
130
+ return 0
131
+
132
+ video_count = user_data["userInfo"]["stats"]["videoCount"]
133
+ if video_count == 0:
134
+ print(f"{userName} has no videos.")
135
+ return 0
136
+ print(f"{userName} has {video_count} videos.")
137
+
138
+ total_views = 0
139
+ total_videos_with_tag = 0
140
+ blackListI = 0
141
+ async for video in user.videos(count=video_count):
142
+ if video.id in videoBlacklist:
143
+ blackListI += 1
144
+ continue
145
+ try:
146
+ # TODO: check if video is in a black list
147
+ play_count = int(video.stats.get("playCount", 0)) # Handle potential missing data
148
+ if any(str(h.name).lower() == hashtag for h in video.hashtags):
149
+
150
+ total_views += play_count
151
+ total_videos_with_tag += 1
152
+
153
+ except (KeyError, TypeError, ValueError) as e:
154
+ print(f"Error processing video for {userName}: {e}")
155
+ return 0 # Skip to the next video if there's an error
156
+
157
+ endTime = time.time()
158
+ tabs = ""
159
+ for _ in range(int(24 - len(userName))):
160
+ tabs += " "
161
+
162
+ print(f"\tTotal views for \033[33m{userName}\033[0m:{tabs} \033[32m{total_views}\033[0m \ttotal videos with tag: \033[35m{total_videos_with_tag}\033[0m \t total videos: \033[36m{video_count}\033[0m process time: \033[31m{round(endTime - startTime, 4)}\033[0m \tblacklisted: \033[31m{blackListI}\033[0m")
163
+ return {"username": userName, "total_views": total_views, "total_videos_with_tag": total_videos_with_tag}
164
+ except Exception as e:
165
+ print(f"An unexpected error occurred for {userName}: {e}")
166
+ return 0 # Skip to the next video if there's an error
167
+
168
+
169
+
170
+
TikTok/TikTokApi/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .tiktok import TikTokApi
TikTok/TikTokApi/api/__init__.py ADDED
File without changes
TikTok/TikTokApi/api/comment.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar, Iterator, Optional
4
+ from typing import TYPE_CHECKING, ClassVar, Optional
5
+
6
+ from TikTok.TikTokApi.exceptions import InvalidResponseException
7
+
8
+ if TYPE_CHECKING:
9
+ from ..tiktok import TikTokApi
10
+ from .user import User
11
+
12
+
13
+ class Comment:
14
+ """
15
+ A TikTok Comment.
16
+
17
+ Example Usage
18
+ .. code-block:: python
19
+
20
+ for comment in video.comments:
21
+ print(comment.text)
22
+ print(comment.as_dict)
23
+ """
24
+
25
+ parent: ClassVar[TikTokApi]
26
+
27
+ id: str
28
+ """The id of the comment"""
29
+ author: ClassVar[User]
30
+ """The author of the comment"""
31
+ text: str
32
+ """The contents of the comment"""
33
+ likes_count: int
34
+ """The amount of likes of the comment"""
35
+ as_dict: dict
36
+ """The raw data associated with this comment"""
37
+
38
+ def __init__(self, data: Optional[dict] = None):
39
+ if data is not None:
40
+ self.as_dict = data
41
+ self.__extract_from_data()
42
+
43
+ def __extract_from_data(self):
44
+ data = self.as_dict
45
+ self.id = self.as_dict["cid"]
46
+ self.text = self.as_dict["text"]
47
+
48
+ usr = self.as_dict["user"]
49
+ self.author = self.parent.user(
50
+ user_id=usr["uid"], username=usr["unique_id"], sec_uid=usr["sec_uid"]
51
+ )
52
+ self.likes_count = self.as_dict["digg_count"]
53
+
54
+ async def replies(self, count=20, cursor=0, **kwargs) -> Iterator[Comment]:
55
+ found = 0
56
+
57
+ while found < count:
58
+ params = {
59
+ "count": 20,
60
+ "cursor": cursor,
61
+ "item_id": self.author.user_id,
62
+ "comment_id": self.id,
63
+ }
64
+
65
+ resp = await self.parent.make_request(
66
+ url="https://www.tiktok.com/api/comment/list/reply/",
67
+ params=params,
68
+ headers=kwargs.get("headers"),
69
+ session_index=kwargs.get("session_index"),
70
+ )
71
+
72
+ if resp is None:
73
+ raise InvalidResponseException(
74
+ resp, "TikTok returned an invalid response."
75
+ )
76
+
77
+ for comment in resp.get("comments", []):
78
+ yield self.parent.comment(data=comment)
79
+ found += 1
80
+
81
+ if not resp.get("has_more", False):
82
+ return
83
+
84
+ cursor = resp.get("cursor")
85
+
86
+ def __repr__(self):
87
+ return self.__str__()
88
+
89
+ def __str__(self):
90
+ id = getattr(self, "id", None)
91
+ text = getattr(self, "text", None)
92
+ return f"TikTokApi.comment(comment_id='{id}', text='{text}')"
TikTok/TikTokApi/api/hashtag.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from ..exceptions import *
3
+
4
+ from typing import TYPE_CHECKING, ClassVar, Iterator, Optional
5
+
6
+ if TYPE_CHECKING:
7
+ from ..tiktok import TikTokApi
8
+ from .video import Video
9
+
10
+
11
+ class Hashtag:
12
+ """
13
+ A TikTok Hashtag/Challenge.
14
+
15
+ Example Usage
16
+ .. code-block:: python
17
+
18
+ hashtag = api.hashtag(name='funny')
19
+ async for video in hashtag.videos():
20
+ print(video.id)
21
+ """
22
+
23
+ parent: ClassVar[TikTokApi]
24
+
25
+ id: Optional[str]
26
+ """The ID of the hashtag"""
27
+ name: Optional[str]
28
+ """The name of the hashtag (omiting the #)"""
29
+ as_dict: dict
30
+ """The raw data associated with this hashtag."""
31
+
32
+ def __init__(
33
+ self,
34
+ name: Optional[str] = None,
35
+ id: Optional[str] = None,
36
+ data: Optional[dict] = None,
37
+ ):
38
+ """
39
+ You must provide the name or id of the hashtag.
40
+ """
41
+
42
+ if name is not None:
43
+ self.name = name
44
+ if id is not None:
45
+ self.id = id
46
+
47
+ if data is not None:
48
+ self.as_dict = data
49
+ self.__extract_from_data()
50
+
51
+ async def info(self, **kwargs) -> dict:
52
+ """
53
+ Returns all information sent by TikTok related to this hashtag.
54
+
55
+ Example Usage
56
+ .. code-block:: python
57
+
58
+ hashtag = api.hashtag(name='funny')
59
+ hashtag_data = await hashtag.info()
60
+ """
61
+ if not self.name:
62
+ raise TypeError(
63
+ "You must provide the name when creating this class to use this method."
64
+ )
65
+
66
+ url_params = {
67
+ "challengeName": self.name,
68
+ "msToken": kwargs.get("ms_token"),
69
+ }
70
+
71
+ resp = await self.parent.make_request(
72
+ url="https://www.tiktok.com/api/challenge/detail/",
73
+ params=url_params,
74
+ headers=kwargs.get("headers"),
75
+ session_index=kwargs.get("session_index"),
76
+ )
77
+
78
+ if resp is None:
79
+ raise InvalidResponseException(resp, "TikTok returned an invalid response.")
80
+
81
+ self.as_dict = resp
82
+ self.__extract_from_data()
83
+ return resp
84
+
85
+ async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]:
86
+ """
87
+ Returns TikTok videos that have this hashtag in the caption.
88
+
89
+ Args:
90
+ count (int): The amount of videos you want returned.
91
+ cursor (int): The the offset of videos from 0 you want to get.
92
+
93
+ Returns:
94
+ async iterator/generator: Yields TikTokApi.video objects.
95
+
96
+ Raises:
97
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
98
+
99
+ Example Usage:
100
+ .. code-block:: python
101
+
102
+ async for video in api.hashtag(name='funny').videos():
103
+ # do something
104
+ """
105
+
106
+ id = getattr(self, "id", None)
107
+ if id is None:
108
+ await self.info(**kwargs)
109
+
110
+ found = 0
111
+ while found < count:
112
+ params = {
113
+ "challengeID": self.id,
114
+ "count": 35,
115
+ "cursor": cursor,
116
+ }
117
+
118
+ resp = await self.parent.make_request(
119
+ url="https://www.tiktok.com/api/challenge/item_list/",
120
+ params=params,
121
+ headers=kwargs.get("headers"),
122
+ session_index=kwargs.get("session_index"),
123
+ )
124
+
125
+ if resp is None:
126
+ raise InvalidResponseException(
127
+ resp, "TikTok returned an invalid response."
128
+ )
129
+
130
+ for video in resp.get("itemList", []):
131
+ yield self.parent.video(data=video)
132
+ found += 1
133
+
134
+ if not resp.get("hasMore", False):
135
+ return
136
+
137
+ cursor = resp.get("cursor")
138
+
139
+ def __extract_from_data(self):
140
+ data = self.as_dict
141
+ keys = data.keys()
142
+
143
+ if "title" in keys:
144
+ self.id = data["id"]
145
+ self.name = data["title"]
146
+
147
+ if "challengeInfo" in keys:
148
+ if "challenge" in data["challengeInfo"]:
149
+ self.id = data["challengeInfo"]["challenge"]["id"]
150
+ self.name = data["challengeInfo"]["challenge"]["title"]
151
+ self.split_name = data["challengeInfo"]["challenge"].get("splitTitle")
152
+
153
+ if "stats" in data["challengeInfo"]:
154
+ self.stats = data["challengeInfo"]["stats"]
155
+
156
+ id = getattr(self, "id", None)
157
+ name = getattr(self, "name", None)
158
+ if None in (id, name):
159
+ Hashtag.parent.logger.error(
160
+ f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}"
161
+ )
162
+
163
+ def __repr__(self):
164
+ return self.__str__()
165
+
166
+ def __str__(self):
167
+ return f"TikTokApi.hashtag(id='{getattr(self, 'id', None)}', name='{getattr(self, 'name', None)}')"
TikTok/TikTokApi/api/search.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from urllib.parse import urlencode
3
+ from typing import TYPE_CHECKING, Iterator
4
+ from .user import User
5
+ from ..exceptions import InvalidResponseException
6
+
7
+ if TYPE_CHECKING:
8
+ from ..tiktok import TikTokApi
9
+
10
+
11
+ class Search:
12
+ """Contains static methods about searching TikTok for a phrase."""
13
+
14
+ parent: TikTokApi
15
+
16
+ @staticmethod
17
+ async def users(search_term, count=10, cursor=0, **kwargs) -> Iterator[User]:
18
+ """
19
+ Searches for users.
20
+
21
+ Note: Your ms_token needs to have done a search before for this to work.
22
+
23
+ Args:
24
+ search_term (str): The phrase you want to search for.
25
+ count (int): The amount of users you want returned.
26
+
27
+ Returns:
28
+ async iterator/generator: Yields TikTokApi.user objects.
29
+
30
+ Raises:
31
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
32
+
33
+ Example Usage:
34
+ .. code-block:: python
35
+
36
+ async for user in api.search.users('david teather'):
37
+ # do something
38
+ """
39
+ async for user in Search.search_type(
40
+ search_term, "user", count=count, cursor=cursor, **kwargs
41
+ ):
42
+ yield user
43
+
44
+ @staticmethod
45
+ async def search_type(
46
+ search_term, obj_type, count=10, cursor=0, **kwargs
47
+ ) -> Iterator:
48
+ """
49
+ Searches for a specific type of object. But you shouldn't use this directly, use the other methods.
50
+
51
+ Note: Your ms_token needs to have done a search before for this to work.
52
+ Note: Currently only supports searching for users, other endpoints require auth.
53
+
54
+ Args:
55
+ search_term (str): The phrase you want to search for.
56
+ obj_type (str): The type of object you want to search for (user)
57
+ count (int): The amount of users you want returned.
58
+ cursor (int): The the offset of users from 0 you want to get.
59
+
60
+ Returns:
61
+ async iterator/generator: Yields TikTokApi.video objects.
62
+
63
+ Raises:
64
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
65
+
66
+ Example Usage:
67
+ .. code-block:: python
68
+
69
+ async for user in api.search.search_type('david teather', 'user'):
70
+ # do something
71
+ """
72
+ found = 0
73
+ while found < count:
74
+ params = {
75
+ "keyword": search_term,
76
+ "cursor": cursor,
77
+ "from_page": "search",
78
+ "web_search_code": """{"tiktok":{"client_params_x":{"search_engine":{"ies_mt_user_live_video_card_use_libra":1,"mt_search_general_user_live_card":1}},"search_server":{}}}""",
79
+ }
80
+
81
+ resp = await Search.parent.make_request(
82
+ url=f"https://www.tiktok.com/api/search/{obj_type}/full/",
83
+ params=params,
84
+ headers=kwargs.get("headers"),
85
+ session_index=kwargs.get("session_index"),
86
+ )
87
+
88
+ if resp is None:
89
+ raise InvalidResponseException(
90
+ resp, "TikTok returned an invalid response."
91
+ )
92
+
93
+ if obj_type == "user":
94
+ for user in resp.get("user_list", []):
95
+ sec_uid = user.get("user_info").get("sec_uid")
96
+ uid = user.get("user_info").get("user_id")
97
+ username = user.get("user_info").get("unique_id")
98
+ yield Search.parent.user(
99
+ sec_uid=sec_uid, user_id=uid, username=username
100
+ )
101
+ found += 1
102
+
103
+ if not resp.get("has_more", False):
104
+ return
105
+
106
+ cursor = resp.get("cursor")
TikTok/TikTokApi/api/sound.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from ..exceptions import *
3
+ from typing import TYPE_CHECKING, ClassVar, Iterator, Optional
4
+
5
+ if TYPE_CHECKING:
6
+ from ..tiktok import TikTokApi
7
+ from .user import User
8
+ from .video import Video
9
+
10
+
11
+ class Sound:
12
+ """
13
+ A TikTok Sound/Music/Song.
14
+
15
+ Example Usage
16
+ .. code-block:: python
17
+
18
+ song = api.song(id='7016547803243022337')
19
+ """
20
+
21
+ parent: ClassVar[TikTokApi]
22
+
23
+ id: str
24
+ """TikTok's ID for the sound"""
25
+ title: Optional[str]
26
+ """The title of the song."""
27
+ author: Optional[User]
28
+ """The author of the song (if it exists)"""
29
+ duration: Optional[int]
30
+ """The duration of the song in seconds."""
31
+ original: Optional[bool]
32
+ """Whether the song is original or not."""
33
+
34
+ def __init__(self, id: Optional[str] = None, data: Optional[str] = None):
35
+ """
36
+ You must provide the id of the sound or it will not work.
37
+ """
38
+ if data is not None:
39
+ self.as_dict = data
40
+ self.__extract_from_data()
41
+ elif id is None:
42
+ raise TypeError("You must provide id parameter.")
43
+ else:
44
+ self.id = id
45
+
46
+ async def info(self, **kwargs) -> dict:
47
+ """
48
+ Returns all information sent by TikTok related to this sound.
49
+
50
+ Returns:
51
+ dict: The raw data returned by TikTok.
52
+
53
+ Raises:
54
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
55
+
56
+ Example Usage:
57
+ .. code-block:: python
58
+
59
+ sound_info = await api.sound(id='7016547803243022337').info()
60
+ """
61
+
62
+ id = getattr(self, "id", None)
63
+ if not id:
64
+ raise TypeError(
65
+ "You must provide the id when creating this class to use this method."
66
+ )
67
+
68
+ url_params = {
69
+ "msToken": kwargs.get("ms_token"),
70
+ "musicId": id,
71
+ }
72
+
73
+ resp = await self.parent.make_request(
74
+ url="https://www.tiktok.com/api/music/detail/",
75
+ params=url_params,
76
+ headers=kwargs.get("headers"),
77
+ session_index=kwargs.get("session_index"),
78
+ )
79
+
80
+ if resp is None:
81
+ raise InvalidResponseException(resp, "TikTok returned an invalid response.")
82
+
83
+ self.as_dict = resp
84
+ self.__extract_from_data()
85
+ return resp
86
+
87
+ async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]:
88
+ """
89
+ Returns Video objects of videos created with this sound.
90
+
91
+ Args:
92
+ count (int): The amount of videos you want returned.
93
+ cursor (int): The the offset of videos from 0 you want to get.
94
+
95
+ Returns:
96
+ async iterator/generator: Yields TikTokApi.video objects.
97
+
98
+ Raises:
99
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
100
+
101
+ Example Usage:
102
+ .. code-block:: python
103
+
104
+ async for video in api.sound(id='7016547803243022337').videos():
105
+ # do something
106
+ """
107
+ id = getattr(self, "id", None)
108
+ if id is None:
109
+ raise TypeError(
110
+ "You must provide the id when creating this class to use this method."
111
+ )
112
+
113
+ found = 0
114
+ while found < count:
115
+ params = {
116
+ "musicID": id,
117
+ "count": 30,
118
+ "cursor": cursor,
119
+ }
120
+
121
+ resp = await self.parent.make_request(
122
+ url="https://www.tiktok.com/api/music/item_list/",
123
+ params=params,
124
+ headers=kwargs.get("headers"),
125
+ session_index=kwargs.get("session_index"),
126
+ )
127
+
128
+ if resp is None:
129
+ raise InvalidResponseException(
130
+ resp, "TikTok returned an invalid response."
131
+ )
132
+
133
+ for video in resp.get("itemList", []):
134
+ yield self.parent.video(data=video)
135
+ found += 1
136
+
137
+ if not resp.get("hasMore", False):
138
+ return
139
+
140
+ cursor = resp.get("cursor")
141
+
142
+ def __extract_from_data(self):
143
+ data = self.as_dict
144
+ keys = data.keys()
145
+
146
+ if "musicInfo" in keys:
147
+ author = data.get("musicInfo").get("author")
148
+ if isinstance(author, dict):
149
+ self.author = self.parent.user(data=author)
150
+ elif isinstance(author, str):
151
+ self.author = self.parent.user(username=author)
152
+
153
+ if data.get("musicInfo").get("music"):
154
+ self.title = data.get("musicInfo").get("music").get("title")
155
+ self.id = data.get("musicInfo").get("music").get("id")
156
+ self.original = data.get("musicInfo").get("music").get("original")
157
+ self.play_url = data.get("musicInfo").get("music").get("playUrl")
158
+ self.cover_large = data.get("musicInfo").get("music").get("coverLarge")
159
+ self.duration = data.get("musicInfo").get("music").get("duration")
160
+
161
+ if "music" in keys:
162
+ self.title = data.get("music").get("title")
163
+ self.id = data.get("music").get("id")
164
+ self.original = data.get("music").get("original")
165
+ self.play_url = data.get("music").get("playUrl")
166
+ self.cover_large = data.get("music").get("coverLarge")
167
+ self.duration = data.get("music").get("duration")
168
+
169
+ if "stats" in keys:
170
+ self.stats = data.get("stats")
171
+
172
+ if getattr(self, "id", None) is None:
173
+ Sound.parent.logger.error(f"Failed to create Sound with data: {data}\n")
174
+
175
+ def __repr__(self):
176
+ return self.__str__()
177
+
178
+ def __str__(self):
179
+ return f"TikTokApi.sound(id='{getattr(self, 'id', None)}')"
TikTok/TikTokApi/api/trending.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from ..exceptions import InvalidResponseException
3
+ from .video import Video
4
+
5
+ from typing import TYPE_CHECKING, Iterator
6
+
7
+ if TYPE_CHECKING:
8
+ from ..tiktok import TikTokApi
9
+
10
+
11
+ class Trending:
12
+ """Contains static methods related to trending objects on TikTok."""
13
+
14
+ parent: TikTokApi
15
+
16
+ @staticmethod
17
+ async def videos(count=30, **kwargs) -> Iterator[Video]:
18
+ """
19
+ Returns Videos that are trending on TikTok.
20
+
21
+ Args:
22
+ count (int): The amount of videos you want returned.
23
+
24
+ Returns:
25
+ async iterator/generator: Yields TikTokApi.video objects.
26
+
27
+ Raises:
28
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
29
+
30
+ Example Usage:
31
+ .. code-block:: python
32
+
33
+ async for video in api.trending.videos():
34
+ # do something
35
+ """
36
+ found = 0
37
+ while found < count:
38
+ params = {
39
+ "from_page": "fyp",
40
+ "count": count,
41
+ }
42
+
43
+ resp = await Trending.parent.make_request(
44
+ url="https://www.tiktok.com/api/recommend/item_list/",
45
+ params=params,
46
+ headers=kwargs.get("headers"),
47
+ session_index=kwargs.get("session_index"),
48
+ )
49
+
50
+ if resp is None:
51
+ raise InvalidResponseException(
52
+ resp, "TikTok returned an invalid response."
53
+ )
54
+
55
+ for video in resp.get("itemList", []):
56
+ yield Trending.parent.video(data=video)
57
+ found += 1
58
+
59
+ if not resp.get("hasMore", False):
60
+ return
TikTok/TikTokApi/api/user.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING, ClassVar, Iterator, Optional
3
+ from ..exceptions import InvalidResponseException
4
+
5
+ if TYPE_CHECKING:
6
+ from ..tiktok import TikTokApi
7
+ from .video import Video
8
+
9
+
10
+ class User:
11
+ """
12
+ A TikTok User.
13
+
14
+ Example Usage:
15
+ .. code-block:: python
16
+
17
+ user = api.user(username='therock')
18
+ """
19
+
20
+ parent: ClassVar[TikTokApi]
21
+
22
+ user_id: str
23
+ """The ID of the user."""
24
+ sec_uid: str
25
+ """The sec UID of the user."""
26
+ username: str
27
+ """The username of the user."""
28
+ as_dict: dict
29
+ """The raw data associated with this user."""
30
+
31
+ def __init__(
32
+ self,
33
+ username: Optional[str] = None,
34
+ user_id: Optional[str] = None,
35
+ sec_uid: Optional[str] = None,
36
+ data: Optional[dict] = None,
37
+ ):
38
+ """
39
+ You must provide the username or (user_id and sec_uid) otherwise this
40
+ will not function correctly.
41
+ """
42
+ self.__update_id_sec_uid_username(user_id, sec_uid, username)
43
+ if data is not None:
44
+ self.as_dict = data
45
+ self.__extract_from_data()
46
+
47
+ async def info(self, **kwargs) -> dict:
48
+ """
49
+ Returns a dictionary of information associated with this User.
50
+
51
+ Returns:
52
+ dict: A dictionary of information associated with this User.
53
+
54
+ Raises:
55
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
56
+
57
+ Example Usage:
58
+ .. code-block:: python
59
+
60
+ user_data = await api.user(username='therock').info()
61
+ """
62
+
63
+ username = getattr(self, "username", None)
64
+ if not username:
65
+ raise TypeError(
66
+ "You must provide the username when creating this class to use this method."
67
+ )
68
+
69
+ sec_uid = getattr(self, "sec_uid", None)
70
+ url_params = {
71
+ "secUid": sec_uid if sec_uid is not None else "",
72
+ "uniqueId": username,
73
+ "msToken": kwargs.get("ms_token"),
74
+ }
75
+
76
+ resp = await self.parent.make_request(
77
+ url="https://www.tiktok.com/api/user/detail/",
78
+ params=url_params,
79
+ headers=kwargs.get("headers"),
80
+ session_index=kwargs.get("session_index"),
81
+ )
82
+
83
+ if resp is None:
84
+ raise InvalidResponseException(resp, "TikTok returned an invalid response.")
85
+
86
+ self.as_dict = resp
87
+ self.__extract_from_data()
88
+ return resp
89
+
90
+ async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[dict]:
91
+ """
92
+ Returns a dictionary of information associated with this User's playlist.
93
+
94
+ Returns:
95
+ dict: A dictionary of information associated with this User's playlist.
96
+
97
+ Raises:
98
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
99
+
100
+ Example Usage:
101
+ .. code-block:: python
102
+
103
+ user_data = await api.user(username='therock').playlist()
104
+ """
105
+
106
+ sec_uid = getattr(self, "sec_uid", None)
107
+ if sec_uid is None or sec_uid == "":
108
+ await self.info(**kwargs)
109
+ found = 0
110
+
111
+ while found < count:
112
+ params = {
113
+ "secUid": sec_uid,
114
+ "count": 20,
115
+ "cursor": cursor,
116
+ }
117
+
118
+ resp = await self.parent.make_request(
119
+ url="https://www.tiktok.com/api/user/playlist",
120
+ params=params,
121
+ headers=kwargs.get("headers"),
122
+ session_index=kwargs.get("session_index"),
123
+ )
124
+
125
+ if resp is None:
126
+ raise InvalidResponseException(resp, "TikTok returned an invalid response.")
127
+
128
+ for playlist in resp.get("playList", []):
129
+ yield playlist
130
+ found += 1
131
+
132
+ if not resp.get("hasMore", False):
133
+ return
134
+
135
+ cursor = resp.get("cursor")
136
+
137
+
138
+ async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]:
139
+ """
140
+ Returns a user's videos.
141
+
142
+ Args:
143
+ count (int): The amount of videos you want returned.
144
+ cursor (int): The the offset of videos from 0 you want to get.
145
+
146
+ Returns:
147
+ async iterator/generator: Yields TikTokApi.video objects.
148
+
149
+ Raises:
150
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
151
+
152
+ Example Usage:
153
+ .. code-block:: python
154
+
155
+ async for video in api.user(username="davidteathercodes").videos():
156
+ # do something
157
+ """
158
+ sec_uid = getattr(self, "sec_uid", None)
159
+ if sec_uid is None or sec_uid == "":
160
+ await self.info(**kwargs)
161
+
162
+ found = 0
163
+ while found < count:
164
+ params = {
165
+ "secUid": self.sec_uid,
166
+ "count": 35,
167
+ "cursor": cursor,
168
+ }
169
+
170
+ resp = await self.parent.make_request(
171
+ url="https://www.tiktok.com/api/post/item_list/",
172
+ params=params,
173
+ headers=kwargs.get("headers"),
174
+ session_index=kwargs.get("session_index"),
175
+ )
176
+
177
+ if resp is None:
178
+ raise InvalidResponseException(
179
+ resp, "TikTok returned an invalid response."
180
+ )
181
+
182
+ for video in resp.get("itemList", []):
183
+ yield self.parent.video(data=video)
184
+ found += 1
185
+
186
+ if not resp.get("hasMore", False):
187
+ return
188
+
189
+ cursor = resp.get("cursor")
190
+
191
+ async def liked(
192
+ self, count: int = 30, cursor: int = 0, **kwargs
193
+ ) -> Iterator[Video]:
194
+ """
195
+ Returns a user's liked posts if public.
196
+
197
+ Args:
198
+ count (int): The amount of recent likes you want returned.
199
+ cursor (int): The the offset of likes from 0 you want to get.
200
+
201
+ Returns:
202
+ async iterator/generator: Yields TikTokApi.video objects.
203
+
204
+ Raises:
205
+ InvalidResponseException: If TikTok returns an invalid response, the user's likes are private, or one we don't understand.
206
+
207
+ Example Usage:
208
+ .. code-block:: python
209
+
210
+ async for like in api.user(username="davidteathercodes").liked():
211
+ # do something
212
+ """
213
+ sec_uid = getattr(self, "sec_uid", None)
214
+ if sec_uid is None or sec_uid == "":
215
+ await self.info(**kwargs)
216
+
217
+ found = 0
218
+ while found < count:
219
+ params = {
220
+ "secUid": self.sec_uid,
221
+ "count": 35,
222
+ "cursor": cursor,
223
+ }
224
+
225
+ resp = await self.parent.make_request(
226
+ url="https://www.tiktok.com/api/favorite/item_list",
227
+ params=params,
228
+ headers=kwargs.get("headers"),
229
+ session_index=kwargs.get("session_index"),
230
+ )
231
+
232
+ if resp is None:
233
+ raise InvalidResponseException(
234
+ resp, "TikTok returned an invalid response."
235
+ )
236
+
237
+ for video in resp.get("itemList", []):
238
+ yield self.parent.video(data=video)
239
+ found += 1
240
+
241
+ if not resp.get("hasMore", False):
242
+ return
243
+
244
+ cursor = resp.get("cursor")
245
+
246
+ def __extract_from_data(self):
247
+ data = self.as_dict
248
+ keys = data.keys()
249
+
250
+ if "userInfo" in keys:
251
+ self.__update_id_sec_uid_username(
252
+ data["userInfo"]["user"]["id"],
253
+ data["userInfo"]["user"]["secUid"],
254
+ data["userInfo"]["user"]["uniqueId"],
255
+ )
256
+ else:
257
+ self.__update_id_sec_uid_username(
258
+ data["id"],
259
+ data["secUid"],
260
+ data["uniqueId"],
261
+ )
262
+
263
+ if None in (self.username, self.user_id, self.sec_uid):
264
+ User.parent.logger.error(
265
+ f"Failed to create User with data: {data}\nwhich has keys {data.keys()}"
266
+ )
267
+
268
+ def __update_id_sec_uid_username(self, id, sec_uid, username):
269
+ self.user_id = id
270
+ self.sec_uid = sec_uid
271
+ self.username = username
272
+
273
+ def __repr__(self):
274
+ return self.__str__()
275
+
276
+ def __str__(self):
277
+ username = getattr(self, "username", None)
278
+ user_id = getattr(self, "user_id", None)
279
+ sec_uid = getattr(self, "sec_uid", None)
280
+ return f"TikTokApi.user(username='{username}', user_id='{user_id}', sec_uid='{sec_uid}')"
TikTok/TikTokApi/api/video.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from ..helpers import extract_video_id_from_url, requests_cookie_to_playwright_cookie
3
+ from typing import TYPE_CHECKING, ClassVar, Iterator, Optional
4
+ from datetime import datetime
5
+ import requests
6
+ from ..exceptions import InvalidResponseException
7
+ import json
8
+ import httpx
9
+ from typing import Union, AsyncIterator
10
+
11
+ if TYPE_CHECKING:
12
+ from ..tiktok import TikTokApi
13
+ from .user import User
14
+ from .sound import Sound
15
+ from .hashtag import Hashtag
16
+ from .comment import Comment
17
+
18
+
19
+ class Video:
20
+ """
21
+ A TikTok Video class
22
+
23
+ Example Usage
24
+ ```py
25
+ video = api.video(id='7041997751718137094')
26
+ ```
27
+ """
28
+
29
+ parent: ClassVar[TikTokApi]
30
+
31
+ id: Optional[str]
32
+ """TikTok's ID of the Video"""
33
+ url: Optional[str]
34
+ """The URL of the Video"""
35
+ create_time: Optional[datetime]
36
+ """The creation time of the Video"""
37
+ stats: Optional[dict]
38
+ """TikTok's stats of the Video"""
39
+ author: Optional[User]
40
+ """The User who created the Video"""
41
+ sound: Optional[Sound]
42
+ """The Sound that is associated with the Video"""
43
+ hashtags: Optional[list[Hashtag]]
44
+ """A List of Hashtags on the Video"""
45
+ as_dict: dict
46
+ """The raw data associated with this Video."""
47
+
48
+ def __init__(
49
+ self,
50
+ id: Optional[str] = None,
51
+ url: Optional[str] = None,
52
+ data: Optional[dict] = None,
53
+ **kwargs,
54
+ ):
55
+ """
56
+ You must provide the id or a valid url, else this will fail.
57
+ """
58
+ self.id = id
59
+ self.url = url
60
+ if data is not None:
61
+ self.as_dict = data
62
+ self.__extract_from_data()
63
+ elif url is not None:
64
+ i, session = self.parent._get_session(**kwargs)
65
+ self.id = extract_video_id_from_url(
66
+ url,
67
+ headers=session.headers,
68
+ proxy=kwargs.get("proxy")
69
+ if kwargs.get("proxy") is not None
70
+ else session.proxy,
71
+ )
72
+
73
+ if getattr(self, "id", None) is None:
74
+ raise TypeError("You must provide id or url parameter.")
75
+
76
+ async def info(self, **kwargs) -> dict:
77
+ """
78
+ Returns a dictionary of all data associated with a TikTok Video.
79
+
80
+ Note: This is slow since it requires an HTTP request, avoid using this if possible.
81
+
82
+ Returns:
83
+ dict: A dictionary of all data associated with a TikTok Video.
84
+
85
+ Raises:
86
+ InvalidResponseException: If TikTok returns an invalid response, or one we don't understand.
87
+
88
+ Example Usage:
89
+ .. code-block:: python
90
+
91
+ url = "https://www.tiktok.com/@davidteathercodes/video/7106686413101468970"
92
+ video_info = await api.video(url=url).info()
93
+ """
94
+ i, session = self.parent._get_session(**kwargs)
95
+ proxy = (
96
+ kwargs.get("proxy") if kwargs.get("proxy") is not None else session.proxy
97
+ )
98
+ if self.url is None:
99
+ raise TypeError("To call video.info() you need to set the video's url.")
100
+
101
+ r = requests.get(self.url, headers=session.headers, proxies=proxy)
102
+ if r.status_code != 200:
103
+ raise InvalidResponseException(
104
+ r.text, "TikTok returned an invalid response.", error_code=r.status_code
105
+ )
106
+
107
+ # Try SIGI_STATE first
108
+ # extract tag <script id="SIGI_STATE" type="application/json">{..}</script>
109
+ # extract json in the middle
110
+
111
+ start = r.text.find('<script id="SIGI_STATE" type="application/json">')
112
+ if start != -1:
113
+ start += len('<script id="SIGI_STATE" type="application/json">')
114
+ end = r.text.find("</script>", start)
115
+
116
+ if end == -1:
117
+ raise InvalidResponseException(
118
+ r.text, "TikTok returned an invalid response.", error_code=r.status_code
119
+ )
120
+
121
+ data = json.loads(r.text[start:end])
122
+ video_info = data["ItemModule"][self.id]
123
+ else:
124
+ # Try __UNIVERSAL_DATA_FOR_REHYDRATION__ next
125
+
126
+ # extract tag <script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">{..}</script>
127
+ # extract json in the middle
128
+
129
+ start = r.text.find('<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">')
130
+ if start == -1:
131
+ raise InvalidResponseException(
132
+ r.text, "TikTok returned an invalid response.", error_code=r.status_code
133
+ )
134
+
135
+ start += len('<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">')
136
+ end = r.text.find("</script>", start)
137
+
138
+ if end == -1:
139
+ raise InvalidResponseException(
140
+ r.text, "TikTok returned an invalid response.", error_code=r.status_code
141
+ )
142
+
143
+ data = json.loads(r.text[start:end])
144
+ default_scope = data.get("__DEFAULT_SCOPE__", {})
145
+ video_detail = default_scope.get("webapp.video-detail", {})
146
+ if video_detail.get("statusCode", 0) != 0: # assume 0 if not present
147
+ raise InvalidResponseException(
148
+ r.text, "TikTok returned an invalid response structure.", error_code=r.status_code
149
+ )
150
+ video_info = video_detail.get("itemInfo", {}).get("itemStruct")
151
+ if video_info is None:
152
+ raise InvalidResponseException(
153
+ r.text, "TikTok returned an invalid response structure.", error_code=r.status_code
154
+ )
155
+
156
+ self.as_dict = video_info
157
+ self.__extract_from_data()
158
+
159
+ cookies = [requests_cookie_to_playwright_cookie(c) for c in r.cookies]
160
+
161
+ await self.parent.set_session_cookies(
162
+ session,
163
+ cookies
164
+ )
165
+ return video_info
166
+
167
+ async def bytes(self, stream: bool = False, **kwargs) -> Union[bytes, AsyncIterator[bytes]]:
168
+ """
169
+ Returns the bytes of a TikTok Video.
170
+
171
+ TODO:
172
+ Not implemented yet.
173
+
174
+ Example Usage:
175
+ .. code-block:: python
176
+
177
+ video_bytes = await api.video(id='7041997751718137094').bytes()
178
+
179
+ # Saving The Video
180
+ with open('saved_video.mp4', 'wb') as output:
181
+ output.write(video_bytes)
182
+
183
+ # Streaming (if stream=True)
184
+ async for chunk in api.video(id='7041997751718137094').bytes(stream=True):
185
+ # Process or upload chunk
186
+ """
187
+ i, session = self.parent._get_session(**kwargs)
188
+ downloadAddr = self.as_dict["video"]["downloadAddr"]
189
+
190
+ cookies = await self.parent.get_session_cookies(session)
191
+
192
+ h = session.headers
193
+ h["range"] = 'bytes=0-'
194
+ h["accept-encoding"] = 'identity;q=1, *;q=0'
195
+ h["referer"] = 'https://www.tiktok.com/'
196
+
197
+ if stream:
198
+ async def stream_bytes():
199
+ async with httpx.AsyncClient() as client:
200
+ async with client.stream('GET', downloadAddr, headers=h, cookies=cookies) as response:
201
+ async for chunk in response.aiter_bytes():
202
+ yield chunk
203
+ return stream_bytes()
204
+ else:
205
+ resp = requests.get(downloadAddr, headers=h, cookies=cookies)
206
+ return resp.content
207
+
208
+ def __extract_from_data(self) -> None:
209
+ data = self.as_dict
210
+ self.id = data["id"]
211
+
212
+ timestamp = data.get("createTime", None)
213
+ if timestamp is not None:
214
+ try:
215
+ timestamp = int(timestamp)
216
+ except ValueError:
217
+ pass
218
+
219
+ self.create_time = datetime.fromtimestamp(timestamp)
220
+ self.stats = data.get('statsV2') or data.get('stats')
221
+
222
+ author = data.get("author")
223
+ if isinstance(author, str):
224
+ self.author = self.parent.user(username=author)
225
+ else:
226
+ self.author = self.parent.user(data=author)
227
+ self.sound = self.parent.sound(data=data)
228
+
229
+ self.hashtags = [
230
+ self.parent.hashtag(data=hashtag) for hashtag in data.get("challenges", [])
231
+ ]
232
+
233
+ if getattr(self, "id", None) is None:
234
+ Video.parent.logger.error(
235
+ f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}"
236
+ )
237
+
238
+ async def comments(self, count=20, cursor=0, **kwargs) -> Iterator[Comment]:
239
+ """
240
+ Returns the comments of a TikTok Video.
241
+
242
+ Parameters:
243
+ count (int): The amount of comments you want returned.
244
+ cursor (int): The the offset of comments from 0 you want to get.
245
+
246
+ Returns:
247
+ async iterator/generator: Yields TikTokApi.comment objects.
248
+
249
+ Example Usage
250
+ .. code-block:: python
251
+
252
+ async for comment in api.video(id='7041997751718137094').comments():
253
+ # do something
254
+ ```
255
+ """
256
+ found = 0
257
+ while found < count:
258
+ params = {
259
+ "aweme_id": self.id,
260
+ "count": 20,
261
+ "cursor": cursor,
262
+ }
263
+
264
+ resp = await self.parent.make_request(
265
+ url="https://www.tiktok.com/api/comment/list/",
266
+ params=params,
267
+ headers=kwargs.get("headers"),
268
+ session_index=kwargs.get("session_index"),
269
+ )
270
+
271
+ if resp is None:
272
+ raise InvalidResponseException(
273
+ resp, "TikTok returned an invalid response."
274
+ )
275
+
276
+ for video in resp.get("comments", []):
277
+ yield self.parent.comment(data=video)
278
+ found += 1
279
+
280
+ if not resp.get("has_more", False):
281
+ return
282
+
283
+ cursor = resp.get("cursor")
284
+
285
+ async def related_videos(
286
+ self, count: int = 30, cursor: int = 0, **kwargs
287
+ ) -> Iterator[Video]:
288
+ """
289
+ Returns related videos of a TikTok Video.
290
+
291
+ Parameters:
292
+ count (int): The amount of comments you want returned.
293
+ cursor (int): The the offset of comments from 0 you want to get.
294
+
295
+ Returns:
296
+ async iterator/generator: Yields TikTokApi.video objects.
297
+
298
+ Example Usage
299
+ .. code-block:: python
300
+
301
+ async for related_videos in api.video(id='7041997751718137094').related_videos():
302
+ # do something
303
+ ```
304
+ """
305
+ found = 0
306
+ while found < count:
307
+ params = {
308
+ "itemID": self.id,
309
+ "count": 16,
310
+ }
311
+
312
+ resp = await self.parent.make_request(
313
+ url="https://www.tiktok.com/api/related/item_list/",
314
+ params=params,
315
+ headers=kwargs.get("headers"),
316
+ session_index=kwargs.get("session_index"),
317
+ )
318
+
319
+ if resp is None:
320
+ raise InvalidResponseException(
321
+ resp, "TikTok returned an invalid response."
322
+ )
323
+
324
+ for video in resp.get("itemList", []):
325
+ yield self.parent.video(data=video)
326
+ found += 1
327
+
328
+ def __repr__(self):
329
+ return self.__str__()
330
+
331
+ def __str__(self):
332
+ return f"TikTokApi.video(id='{getattr(self, 'id', None)}')"
TikTok/TikTokApi/exceptions.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class TikTokException(Exception):
2
+ """Generic exception that all other TikTok errors are children of."""
3
+
4
+ def __init__(self, raw_response, message, error_code=None):
5
+ self.error_code = error_code
6
+ self.raw_response = raw_response
7
+ self.message = message
8
+ super().__init__(self.message)
9
+
10
+ def __str__(self):
11
+ return f"{self.error_code} -> {self.message}"
12
+
13
+
14
+ class CaptchaException(TikTokException):
15
+ """TikTok is showing captcha"""
16
+
17
+
18
+ class NotFoundException(TikTokException):
19
+ """TikTok indicated that this object does not exist."""
20
+
21
+
22
+ class EmptyResponseException(TikTokException):
23
+ """TikTok sent back an empty response."""
24
+
25
+
26
+ class SoundRemovedException(TikTokException):
27
+ """This TikTok sound has no id from being removed by TikTok."""
28
+
29
+
30
+ class InvalidJSONException(TikTokException):
31
+ """TikTok returned invalid JSON."""
32
+
33
+
34
+ class InvalidResponseException(TikTokException):
35
+ """The response from TikTok was invalid."""
TikTok/TikTokApi/helpers.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .exceptions import *
2
+
3
+ import requests
4
+ import random
5
+
6
+
7
+ def extract_video_id_from_url(url, headers={}, proxy=None):
8
+ url = requests.head(
9
+ url=url, allow_redirects=True, headers=headers, proxies=proxy
10
+ ).url
11
+ if "@" in url and "/video/" in url:
12
+ return url.split("/video/")[1].split("?")[0]
13
+ else:
14
+ raise TypeError(
15
+ "URL format not supported. Below is an example of a supported url.\n"
16
+ "https://www.tiktok.com/@therock/video/6829267836783971589"
17
+ )
18
+
19
+
20
+ def random_choice(choices: list):
21
+ """Return a random choice from a list, or None if the list is empty"""
22
+ if choices is None or len(choices) == 0:
23
+ return None
24
+ return random.choice(choices)
25
+
26
+ def requests_cookie_to_playwright_cookie(req_c):
27
+ c = {
28
+ 'name': req_c.name,
29
+ 'value': req_c.value,
30
+ 'domain': req_c.domain,
31
+ 'path': req_c.path,
32
+ 'secure': req_c.secure
33
+ }
34
+ if req_c.expires:
35
+ c['expires'] = req_c.expires
36
+ return c
TikTok/TikTokApi/stealth/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .stealth import stealth_async
TikTok/TikTokApi/stealth/js/__init__.py ADDED
File without changes
TikTok/TikTokApi/stealth/js/chrome_app.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chrome_app = """
2
+ if (!window.chrome) {
3
+ // Use the exact property descriptor found in headful Chrome
4
+ // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
5
+ Object.defineProperty(window, 'chrome', {
6
+ writable: true,
7
+ enumerable: true,
8
+ configurable: false, // note!
9
+ value: {} // We'll extend that later
10
+ })
11
+ }
12
+
13
+ // app in window.chrome means we're running headful and don't need to mock anything
14
+ if (!('app' in window.chrome)) {
15
+ const makeError = {
16
+ ErrorInInvocation: fn => {
17
+ const err = new TypeError(`Error in invocation of app.${fn}()`)
18
+ return utils.stripErrorWithAnchor(
19
+ err,
20
+ `at ${fn} (eval at <anonymous>`
21
+ )
22
+ }
23
+ }
24
+
25
+ // There's a some static data in that property which doesn't seem to change,
26
+ // we should periodically check for updates: `JSON.stringify(window.app, null, 2)`
27
+ const APP_STATIC_DATA = JSON.parse(
28
+ `
29
+ {
30
+ "isInstalled": false,
31
+ "InstallState": {
32
+ "DISABLED": "disabled",
33
+ "INSTALLED": "installed",
34
+ "NOT_INSTALLED": "not_installed"
35
+ },
36
+ "RunningState": {
37
+ "CANNOT_RUN": "cannot_run",
38
+ "READY_TO_RUN": "ready_to_run",
39
+ "RUNNING": "running"
40
+ }
41
+ }
42
+ `.trim()
43
+ )
44
+
45
+ window.chrome.app = {
46
+ ...APP_STATIC_DATA,
47
+
48
+ get isInstalled() {
49
+ return false
50
+ },
51
+
52
+ getDetails: function getDetails() {
53
+ if (arguments.length) {
54
+ throw makeError.ErrorInInvocation(`getDetails`)
55
+ }
56
+ return null
57
+ },
58
+ getIsInstalled: function getDetails() {
59
+ if (arguments.length) {
60
+ throw makeError.ErrorInInvocation(`getIsInstalled`)
61
+ }
62
+ return false
63
+ },
64
+ runningState: function getDetails() {
65
+ if (arguments.length) {
66
+ throw makeError.ErrorInInvocation(`runningState`)
67
+ }
68
+ return 'cannot_run'
69
+ }
70
+ }
71
+ utils.patchToStringNested(window.chrome.app)
72
+ }
73
+ """
TikTok/TikTokApi/stealth/js/chrome_csi.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chrome_csi = """
2
+ if (!window.chrome) {
3
+ // Use the exact property descriptor found in headful Chrome
4
+ // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
5
+ Object.defineProperty(window, 'chrome', {
6
+ writable: true,
7
+ enumerable: true,
8
+ configurable: false, // note!
9
+ value: {} // We'll extend that later
10
+ })
11
+ }
12
+
13
+ // Check if we're running headful and don't need to mock anything
14
+ // Check that the Navigation Timing API v1 is available, we need that
15
+ if (!('csi' in window.chrome) && (window.performance || window.performance.timing)) {
16
+ const {csi_timing} = window.performance
17
+
18
+ log.info('loading chrome.csi.js')
19
+ window.chrome.csi = function () {
20
+ return {
21
+ onloadT: csi_timing.domContentLoadedEventEnd,
22
+ startE: csi_timing.navigationStart,
23
+ pageT: Date.now() - csi_timing.navigationStart,
24
+ tran: 15 // Transition type or something
25
+ }
26
+ }
27
+ utils.patchToString(window.chrome.csi)
28
+ }
29
+ """
TikTok/TikTokApi/stealth/js/chrome_hairline.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chrome_hairline = """
2
+ // https://intoli.com/blog/making-chrome-headless-undetectable/
3
+ // store the existing descriptor
4
+ const elementDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');
5
+
6
+ // redefine the property with a patched descriptor
7
+ Object.defineProperty(HTMLDivElement.prototype, 'offsetHeight', {
8
+ ...elementDescriptor,
9
+ get: function() {
10
+ if (this.id === 'modernizr') {
11
+ return 1;
12
+ }
13
+ return elementDescriptor.get.apply(this);
14
+ },
15
+ });
16
+ """
TikTok/TikTokApi/stealth/js/chrome_load_times.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chrome_load_times = """
2
+ if (!window.chrome) {
3
+ // Use the exact property descriptor found in headful Chrome
4
+ // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
5
+ Object.defineProperty(window, 'chrome', {
6
+ writable: true,
7
+ enumerable: true,
8
+ configurable: false, // note!
9
+ value: {} // We'll extend that later
10
+ })
11
+ }
12
+
13
+ // That means we're running headful and don't need to mock anything
14
+ if ('loadTimes' in window.chrome) {
15
+ throw new Error('skipping chrome loadtimes update, running in headfull mode')
16
+ }
17
+
18
+ // Check that the Navigation Timing API v1 + v2 is available, we need that
19
+ if (
20
+ window.performance ||
21
+ window.performance.timing ||
22
+ window.PerformancePaintTiming
23
+ ) {
24
+
25
+ const {performance} = window
26
+
27
+ // Some stuff is not available on about:blank as it requires a navigation to occur,
28
+ // let's harden the code to not fail then:
29
+ const ntEntryFallback = {
30
+ nextHopProtocol: 'h2',
31
+ type: 'other'
32
+ }
33
+
34
+ // The API exposes some funky info regarding the connection
35
+ const protocolInfo = {
36
+ get connectionInfo() {
37
+ const ntEntry =
38
+ performance.getEntriesByType('navigation')[0] || ntEntryFallback
39
+ return ntEntry.nextHopProtocol
40
+ },
41
+ get npnNegotiatedProtocol() {
42
+ // NPN is deprecated in favor of ALPN, but this implementation returns the
43
+ // HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
44
+ const ntEntry =
45
+ performance.getEntriesByType('navigation')[0] || ntEntryFallback
46
+ return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)
47
+ ? ntEntry.nextHopProtocol
48
+ : 'unknown'
49
+ },
50
+ get navigationType() {
51
+ const ntEntry =
52
+ performance.getEntriesByType('navigation')[0] || ntEntryFallback
53
+ return ntEntry.type
54
+ },
55
+ get wasAlternateProtocolAvailable() {
56
+ // The Alternate-Protocol header is deprecated in favor of Alt-Svc
57
+ // (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this
58
+ // should always return false.
59
+ return false
60
+ },
61
+ get wasFetchedViaSpdy() {
62
+ // SPDY is deprecated in favor of HTTP/2, but this implementation returns
63
+ // true for HTTP/2 or HTTP2+QUIC/39 as well.
64
+ const ntEntry =
65
+ performance.getEntriesByType('navigation')[0] || ntEntryFallback
66
+ return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)
67
+ },
68
+ get wasNpnNegotiated() {
69
+ // NPN is deprecated in favor of ALPN, but this implementation returns true
70
+ // for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
71
+ const ntEntry =
72
+ performance.getEntriesByType('navigation')[0] || ntEntryFallback
73
+ return ['h2', 'hq'].includes(ntEntry.nextHopProtocol)
74
+ }
75
+ }
76
+
77
+ const {timing} = window.performance
78
+
79
+ // Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3
80
+ function toFixed(num, fixed) {
81
+ var re = new RegExp('^-?\\d+(?:.\\d{0,' + (fixed || -1) + '})?')
82
+ return num.toString().match(re)[0]
83
+ }
84
+
85
+ const timingInfo = {
86
+ get firstPaintAfterLoadTime() {
87
+ // This was never actually implemented and always returns 0.
88
+ return 0
89
+ },
90
+ get requestTime() {
91
+ return timing.navigationStart / 1000
92
+ },
93
+ get startLoadTime() {
94
+ return timing.navigationStart / 1000
95
+ },
96
+ get commitLoadTime() {
97
+ return timing.responseStart / 1000
98
+ },
99
+ get finishDocumentLoadTime() {
100
+ return timing.domContentLoadedEventEnd / 1000
101
+ },
102
+ get finishLoadTime() {
103
+ return timing.loadEventEnd / 1000
104
+ },
105
+ get firstPaintTime() {
106
+ const fpEntry = performance.getEntriesByType('paint')[0] || {
107
+ startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`)
108
+ }
109
+ return toFixed(
110
+ (fpEntry.startTime + performance.timeOrigin) / 1000,
111
+ 3
112
+ )
113
+ }
114
+ }
115
+
116
+ window.chrome.loadTimes = function () {
117
+ return {
118
+ ...protocolInfo,
119
+ ...timingInfo
120
+ }
121
+ }
122
+ utils.patchToString(window.chrome.loadTimes)
123
+ }
124
+ """
TikTok/TikTokApi/stealth/js/chrome_runtime.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chrome_runtime = """
2
+ const STATIC_DATA = {
3
+ "OnInstalledReason": {
4
+ "CHROME_UPDATE": "chrome_update",
5
+ "INSTALL": "install",
6
+ "SHARED_MODULE_UPDATE": "shared_module_update",
7
+ "UPDATE": "update"
8
+ },
9
+ "OnRestartRequiredReason": {
10
+ "APP_UPDATE": "app_update",
11
+ "OS_UPDATE": "os_update",
12
+ "PERIODIC": "periodic"
13
+ },
14
+ "PlatformArch": {
15
+ "ARM": "arm",
16
+ "ARM64": "arm64",
17
+ "MIPS": "mips",
18
+ "MIPS64": "mips64",
19
+ "X86_32": "x86-32",
20
+ "X86_64": "x86-64"
21
+ },
22
+ "PlatformNaclArch": {
23
+ "ARM": "arm",
24
+ "MIPS": "mips",
25
+ "MIPS64": "mips64",
26
+ "X86_32": "x86-32",
27
+ "X86_64": "x86-64"
28
+ },
29
+ "PlatformOs": {
30
+ "ANDROID": "android",
31
+ "CROS": "cros",
32
+ "LINUX": "linux",
33
+ "MAC": "mac",
34
+ "OPENBSD": "openbsd",
35
+ "WIN": "win"
36
+ },
37
+ "RequestUpdateCheckStatus": {
38
+ "NO_UPDATE": "no_update",
39
+ "THROTTLED": "throttled",
40
+ "UPDATE_AVAILABLE": "update_available"
41
+ }
42
+ }
43
+
44
+ if (!window.chrome) {
45
+ // Use the exact property descriptor found in headful Chrome
46
+ // fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
47
+ Object.defineProperty(window, 'chrome', {
48
+ writable: true,
49
+ enumerable: true,
50
+ configurable: false, // note!
51
+ value: {} // We'll extend that later
52
+ })
53
+ }
54
+
55
+ // That means we're running headfull and don't need to mock anything
56
+ const existsAlready = 'runtime' in window.chrome
57
+ // `chrome.runtime` is only exposed on secure origins
58
+ const isNotSecure = !window.location.protocol.startsWith('https')
59
+ if (!(existsAlready || (isNotSecure && !opts.runOnInsecureOrigins))) {
60
+ window.chrome.runtime = {
61
+ // There's a bunch of static data in that property which doesn't seem to change,
62
+ // we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`
63
+ ...STATIC_DATA,
64
+ // `chrome.runtime.id` is extension related and returns undefined in Chrome
65
+ get id() {
66
+ return undefined
67
+ },
68
+ // These two require more sophisticated mocks
69
+ connect: null,
70
+ sendMessage: null
71
+ }
72
+
73
+ const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
74
+ NoMatchingSignature: new TypeError(
75
+ preamble + `No matching signature.`
76
+ ),
77
+ MustSpecifyExtensionID: new TypeError(
78
+ preamble +
79
+ `${method} called from a webpage must specify an Extension ID (string) for its first argument.`
80
+ ),
81
+ InvalidExtensionID: new TypeError(
82
+ preamble + `Invalid extension id: '${extensionId}'`
83
+ )
84
+ })
85
+
86
+ // Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:
87
+ // https://source.chromium.org/chromium/chromium/src/+/main:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90
88
+ const isValidExtensionID = str =>
89
+ str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)
90
+
91
+ /** Mock `chrome.runtime.sendMessage` */
92
+ const sendMessageHandler = {
93
+ apply: function (target, ctx, args) {
94
+ const [extensionId, options, responseCallback] = args || []
95
+
96
+ // Define custom errors
97
+ const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `
98
+ const Errors = makeCustomRuntimeErrors(
99
+ errorPreamble,
100
+ `chrome.runtime.sendMessage()`,
101
+ extensionId
102
+ )
103
+
104
+ // Check if the call signature looks ok
105
+ const noArguments = args.length === 0
106
+ const tooManyArguments = args.length > 4
107
+ const incorrectOptions = options && typeof options !== 'object'
108
+ const incorrectResponseCallback =
109
+ responseCallback && typeof responseCallback !== 'function'
110
+ if (
111
+ noArguments ||
112
+ tooManyArguments ||
113
+ incorrectOptions ||
114
+ incorrectResponseCallback
115
+ ) {
116
+ throw Errors.NoMatchingSignature
117
+ }
118
+
119
+ // At least 2 arguments are required before we even validate the extension ID
120
+ if (args.length < 2) {
121
+ throw Errors.MustSpecifyExtensionID
122
+ }
123
+
124
+ // Now let's make sure we got a string as extension ID
125
+ if (typeof extensionId !== 'string') {
126
+ throw Errors.NoMatchingSignature
127
+ }
128
+
129
+ if (!isValidExtensionID(extensionId)) {
130
+ throw Errors.InvalidExtensionID
131
+ }
132
+
133
+ return undefined // Normal behavior
134
+ }
135
+ }
136
+ utils.mockWithProxy(
137
+ window.chrome.runtime,
138
+ 'sendMessage',
139
+ function sendMessage() {
140
+ },
141
+ sendMessageHandler
142
+ )
143
+
144
+ /**
145
+ * Mock `chrome.runtime.connect`
146
+ *
147
+ * @see https://developer.chrome.com/apps/runtime#method-connect
148
+ */
149
+ const connectHandler = {
150
+ apply: function (target, ctx, args) {
151
+ const [extensionId, connectInfo] = args || []
152
+
153
+ // Define custom errors
154
+ const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `
155
+ const Errors = makeCustomRuntimeErrors(
156
+ errorPreamble,
157
+ `chrome.runtime.connect()`,
158
+ extensionId
159
+ )
160
+
161
+ // Behavior differs a bit from sendMessage:
162
+ const noArguments = args.length === 0
163
+ const emptyStringArgument = args.length === 1 && extensionId === ''
164
+ if (noArguments || emptyStringArgument) {
165
+ throw Errors.MustSpecifyExtensionID
166
+ }
167
+
168
+ const tooManyArguments = args.length > 2
169
+ const incorrectConnectInfoType =
170
+ connectInfo && typeof connectInfo !== 'object'
171
+
172
+ if (tooManyArguments || incorrectConnectInfoType) {
173
+ throw Errors.NoMatchingSignature
174
+ }
175
+
176
+ const extensionIdIsString = typeof extensionId === 'string'
177
+ if (extensionIdIsString && extensionId === '') {
178
+ throw Errors.MustSpecifyExtensionID
179
+ }
180
+ if (extensionIdIsString && !isValidExtensionID(extensionId)) {
181
+ throw Errors.InvalidExtensionID
182
+ }
183
+
184
+ // There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate
185
+ const validateConnectInfo = ci => {
186
+ // More than a first param connectInfo as been provided
187
+ if (args.length > 1) {
188
+ throw Errors.NoMatchingSignature
189
+ }
190
+ // An empty connectInfo has been provided
191
+ if (Object.keys(ci).length === 0) {
192
+ throw Errors.MustSpecifyExtensionID
193
+ }
194
+ // Loop over all connectInfo props an check them
195
+ Object.entries(ci).forEach(([k, v]) => {
196
+ const isExpected = ['name', 'includeTlsChannelId'].includes(k)
197
+ if (!isExpected) {
198
+ throw new TypeError(
199
+ errorPreamble + `Unexpected property: '${k}'.`
200
+ )
201
+ }
202
+ const MismatchError = (propName, expected, found) =>
203
+ TypeError(
204
+ errorPreamble +
205
+ `Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`
206
+ )
207
+ if (k === 'name' && typeof v !== 'string') {
208
+ throw MismatchError(k, 'string', typeof v)
209
+ }
210
+ if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {
211
+ throw MismatchError(k, 'boolean', typeof v)
212
+ }
213
+ })
214
+ }
215
+ if (typeof extensionId === 'object') {
216
+ validateConnectInfo(extensionId)
217
+ throw Errors.MustSpecifyExtensionID
218
+ }
219
+
220
+ // Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well
221
+ return utils.patchToStringNested(makeConnectResponse())
222
+ }
223
+ }
224
+ utils.mockWithProxy(
225
+ window.chrome.runtime,
226
+ 'connect',
227
+ function connect() {
228
+ },
229
+ connectHandler
230
+ )
231
+
232
+ function makeConnectResponse() {
233
+ const onSomething = () => ({
234
+ addListener: function addListener() {
235
+ },
236
+ dispatch: function dispatch() {
237
+ },
238
+ hasListener: function hasListener() {
239
+ },
240
+ hasListeners: function hasListeners() {
241
+ return false
242
+ },
243
+ removeListener: function removeListener() {
244
+ }
245
+ })
246
+
247
+ const response = {
248
+ name: '',
249
+ sender: undefined,
250
+ disconnect: function disconnect() {
251
+ },
252
+ onDisconnect: onSomething(),
253
+ onMessage: onSomething(),
254
+ postMessage: function postMessage() {
255
+ if (!arguments.length) {
256
+ throw new TypeError(`Insufficient number of arguments.`)
257
+ }
258
+ throw new Error(`Attempting to use a disconnected port object`)
259
+ }
260
+ }
261
+ return response
262
+ }
263
+ }
264
+
265
+ """
TikTok/TikTokApi/stealth/js/generate_magic_arrays.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generate_magic_arrays = """
2
+ generateFunctionMocks = (
3
+ proto,
4
+ itemMainProp,
5
+ dataArray
6
+ ) => ({
7
+ item: utils.createProxy(proto.item, {
8
+ apply(target, ctx, args) {
9
+ if (!args.length) {
10
+ throw new TypeError(
11
+ `Failed to execute 'item' on '${
12
+ proto[Symbol.toStringTag]
13
+ }': 1 argument required, but only 0 present.`
14
+ )
15
+ }
16
+ // Special behavior alert:
17
+ // - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup
18
+ // - If anything else than an integer (including as string) is provided it will return the first entry
19
+ const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer
20
+ // Note: Vanilla never returns `undefined`
21
+ return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null
22
+ }
23
+ }),
24
+ /** Returns the MimeType object with the specified name. */
25
+ namedItem: utils.createProxy(proto.namedItem, {
26
+ apply(target, ctx, args) {
27
+ if (!args.length) {
28
+ throw new TypeError(
29
+ `Failed to execute 'namedItem' on '${
30
+ proto[Symbol.toStringTag]
31
+ }': 1 argument required, but only 0 present.`
32
+ )
33
+ }
34
+ return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`!
35
+ }
36
+ }),
37
+ /** Does nothing and shall return nothing */
38
+ refresh: proto.refresh
39
+ ? utils.createProxy(proto.refresh, {
40
+ apply(target, ctx, args) {
41
+ return undefined
42
+ }
43
+ })
44
+ : undefined
45
+ })
46
+
47
+ function generateMagicArray(
48
+ dataArray = [],
49
+ proto = MimeTypeArray.prototype,
50
+ itemProto = MimeType.prototype,
51
+ itemMainProp = 'type'
52
+ ) {
53
+ // Quick helper to set props with the same descriptors vanilla is using
54
+ const defineProp = (obj, prop, value) =>
55
+ Object.defineProperty(obj, prop, {
56
+ value,
57
+ writable: false,
58
+ enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`
59
+ configurable: false
60
+ })
61
+
62
+ // Loop over our fake data and construct items
63
+ const makeItem = data => {
64
+ const item = {}
65
+ for (const prop of Object.keys(data)) {
66
+ if (prop.startsWith('__')) {
67
+ continue
68
+ }
69
+ defineProp(item, prop, data[prop])
70
+ }
71
+ // navigator.plugins[i].length should always be 1
72
+ if (itemProto === Plugin.prototype) {
73
+ defineProp(item, 'length', 1)
74
+ }
75
+ // We need to spoof a specific `MimeType` or `Plugin` object
76
+ return Object.create(itemProto, Object.getOwnPropertyDescriptors(item))
77
+ }
78
+
79
+ const magicArray = []
80
+
81
+ // Loop through our fake data and use that to create convincing entities
82
+ dataArray.forEach(data => {
83
+ magicArray.push(makeItem(data))
84
+ })
85
+
86
+ // Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards
87
+ magicArray.forEach(entry => {
88
+ defineProp(magicArray, entry[itemMainProp], entry)
89
+ })
90
+
91
+ // This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`
92
+ const magicArrayObj = Object.create(proto, {
93
+ ...Object.getOwnPropertyDescriptors(magicArray),
94
+
95
+ // There's one ugly quirk we unfortunately need to take care of:
96
+ // The `MimeTypeArray` prototype has an enumerable `length` property,
97
+ // but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.
98
+ // To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.
99
+ length: {
100
+ value: magicArray.length,
101
+ writable: false,
102
+ enumerable: false,
103
+ configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`
104
+ }
105
+ })
106
+
107
+ // Generate our functional function mocks :-)
108
+ const functionMocks = generateFunctionMocks(
109
+ proto,
110
+ itemMainProp,
111
+ magicArray
112
+ )
113
+
114
+ // Override custom object with proxy
115
+ return new Proxy(magicArrayObj, {
116
+ get(target, key = '') {
117
+ // Redirect function calls to our custom proxied versions mocking the vanilla behavior
118
+ if (key === 'item') {
119
+ return functionMocks.item
120
+ }
121
+ if (key === 'namedItem') {
122
+ return functionMocks.namedItem
123
+ }
124
+ if (proto === PluginArray.prototype && key === 'refresh') {
125
+ return functionMocks.refresh
126
+ }
127
+ // Everything else can pass through as normal
128
+ return utils.cache.Reflect.get(...arguments)
129
+ },
130
+ ownKeys(target) {
131
+ // There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense
132
+ // This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`
133
+ // My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly
134
+ // For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing
135
+ // Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing
136
+ const keys = []
137
+ const typeProps = magicArray.map(mt => mt[itemMainProp])
138
+ typeProps.forEach((_, i) => keys.push(`${i}`))
139
+ typeProps.forEach(propName => keys.push(propName))
140
+ return keys
141
+ }
142
+ })
143
+ }
144
+ """
TikTok/TikTokApi/stealth/js/iframe_contentWindow.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ iframe_contentWindow = """
2
+ try {
3
+ // Adds a contentWindow proxy to the provided iframe element
4
+ const addContentWindowProxy = iframe => {
5
+ const contentWindowProxy = {
6
+ get(target, key) {
7
+ // Now to the interesting part:
8
+ // We actually make this thing behave like a regular iframe window,
9
+ // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
10
+ // That makes it possible for these assertions to be correct:
11
+ // iframe.contentWindow.self === window.top // must be false
12
+ if (key === 'self') {
13
+ return this
14
+ }
15
+ // iframe.contentWindow.frameElement === iframe // must be true
16
+ if (key === 'frameElement') {
17
+ return iframe
18
+ }
19
+ return Reflect.get(target, key)
20
+ }
21
+ }
22
+
23
+ if (!iframe.contentWindow) {
24
+ const proxy = new Proxy(window, contentWindowProxy)
25
+ Object.defineProperty(iframe, 'contentWindow', {
26
+ get() {
27
+ return proxy
28
+ },
29
+ set(newValue) {
30
+ return newValue // contentWindow is immutable
31
+ },
32
+ enumerable: true,
33
+ configurable: false
34
+ })
35
+ }
36
+ }
37
+
38
+ // Handles iframe element creation, augments `srcdoc` property so we can intercept further
39
+ const handleIframeCreation = (target, thisArg, args) => {
40
+ const iframe = target.apply(thisArg, args)
41
+
42
+ // We need to keep the originals around
43
+ const _iframe = iframe
44
+ const _srcdoc = _iframe.srcdoc
45
+
46
+ // Add hook for the srcdoc property
47
+ // We need to be very surgical here to not break other iframes by accident
48
+ Object.defineProperty(iframe, 'srcdoc', {
49
+ configurable: true, // Important, so we can reset this later
50
+ get: function () {
51
+ return _iframe.srcdoc
52
+ },
53
+ set: function (newValue) {
54
+ addContentWindowProxy(this)
55
+ // Reset property, the hook is only needed once
56
+ Object.defineProperty(iframe, 'srcdoc', {
57
+ configurable: false,
58
+ writable: false,
59
+ value: _srcdoc
60
+ })
61
+ _iframe.srcdoc = newValue
62
+ }
63
+ })
64
+ return iframe
65
+ }
66
+
67
+ // Adds a hook to intercept iframe creation events
68
+ const addIframeCreationSniffer = () => {
69
+ /* global document */
70
+ const createElementHandler = {
71
+ // Make toString() native
72
+ get(target, key) {
73
+ return Reflect.get(target, key)
74
+ },
75
+ apply: function (target, thisArg, args) {
76
+ const isIframe =
77
+ args && args.length && `${args[0]}`.toLowerCase() === 'iframe'
78
+ if (!isIframe) {
79
+ // Everything as usual
80
+ return target.apply(thisArg, args)
81
+ } else {
82
+ return handleIframeCreation(target, thisArg, args)
83
+ }
84
+ }
85
+ }
86
+ // All this just due to iframes with srcdoc bug
87
+ utils.replaceWithProxy(
88
+ document,
89
+ 'createElement',
90
+ createElementHandler
91
+ )
92
+ }
93
+
94
+ // Let's go
95
+ addIframeCreationSniffer()
96
+ } catch (err) {
97
+ // console.warn(err)
98
+ }
99
+ """
TikTok/TikTokApi/stealth/js/media_codecs.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ media_codecs = """
2
+ /**
3
+ * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
4
+ *
5
+ * @example
6
+ * video/webm; codecs="vp8, vorbis"
7
+ * video/mp4; codecs="avc1.42E01E"
8
+ * audio/x-m4a;
9
+ * audio/ogg; codecs="vorbis"
10
+ * @param {String} arg
11
+ */
12
+ const parseInput = arg => {
13
+ const [mime, codecStr] = arg.trim().split(';')
14
+ let codecs = []
15
+ if (codecStr && codecStr.includes('codecs="')) {
16
+ codecs = codecStr
17
+ .trim()
18
+ .replace(`codecs="`, '')
19
+ .replace(`"`, '')
20
+ .trim()
21
+ .split(',')
22
+ .filter(x => !!x)
23
+ .map(x => x.trim())
24
+ }
25
+ return {
26
+ mime,
27
+ codecStr,
28
+ codecs
29
+ }
30
+ }
31
+
32
+ const canPlayType = {
33
+ // Intercept certain requests
34
+ apply: function (target, ctx, args) {
35
+ if (!args || !args.length) {
36
+ return target.apply(ctx, args)
37
+ }
38
+ const {mime, codecs} = parseInput(args[0])
39
+ // This specific mp4 codec is missing in Chromium
40
+ if (mime === 'video/mp4') {
41
+ if (codecs.includes('avc1.42E01E')) {
42
+ return 'probably'
43
+ }
44
+ }
45
+ // This mimetype is only supported if no codecs are specified
46
+ if (mime === 'audio/x-m4a' && !codecs.length) {
47
+ return 'maybe'
48
+ }
49
+
50
+ // This mimetype is only supported if no codecs are specified
51
+ if (mime === 'audio/aac' && !codecs.length) {
52
+ return 'probably'
53
+ }
54
+ // Everything else as usual
55
+ return target.apply(ctx, args)
56
+ }
57
+ }
58
+
59
+ /* global HTMLMediaElement */
60
+ utils.replaceWithProxy(
61
+ HTMLMediaElement.prototype,
62
+ 'canPlayType',
63
+ canPlayType
64
+ )
65
+ """
TikTok/TikTokApi/stealth/js/navigator_hardwareConcurrency.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ navigator_hardwareConcurrency = """
2
+ const patchNavigator = (name, value) =>
3
+ utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
4
+ get() {
5
+ return value
6
+ }
7
+ })
8
+
9
+ patchNavigator('hardwareConcurrency', opts.navigator_hardware_concurrency || 4);
10
+ """
TikTok/TikTokApi/stealth/js/navigator_languages.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ navigator_languages = """
2
+ Object.defineProperty(Object.getPrototypeOf(navigator), 'languages', {
3
+ get: () => opts.languages || ['en-US', 'en']
4
+ })
5
+
6
+ """
TikTok/TikTokApi/stealth/js/navigator_permissions.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ navigator_permissions = """
2
+ const handler = {
3
+ apply: function (target, ctx, args) {
4
+ const param = (args || [])[0]
5
+
6
+ if (param && param.name && param.name === 'notifications') {
7
+ const result = {state: Notification.permission}
8
+ Object.setPrototypeOf(result, PermissionStatus.prototype)
9
+ return Promise.resolve(result)
10
+ }
11
+
12
+ return utils.cache.Reflect.apply(...arguments)
13
+ }
14
+ }
15
+
16
+ utils.replaceWithProxy(
17
+ window.navigator.permissions.__proto__, // eslint-disable-line no-proto
18
+ 'query',
19
+ handler
20
+ )
21
+
22
+ """
TikTok/TikTokApi/stealth/js/navigator_platform.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ navigator_platform = """
2
+ if (opts.navigator_platform) {
3
+ Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', {
4
+ get: () => opts.navigator_plaftorm,
5
+ })
6
+ }
7
+ """
TikTok/TikTokApi/stealth/js/navigator_plugins.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ navigator_plugins = """
2
+ data = {
3
+ "mimeTypes": [
4
+ {
5
+ "type": "application/pdf",
6
+ "suffixes": "pdf",
7
+ "description": "",
8
+ "__pluginName": "Chrome PDF Viewer"
9
+ },
10
+ {
11
+ "type": "application/x-google-chrome-pdf",
12
+ "suffixes": "pdf",
13
+ "description": "Portable Document Format",
14
+ "__pluginName": "Chrome PDF Plugin"
15
+ },
16
+ {
17
+ "type": "application/x-nacl",
18
+ "suffixes": "",
19
+ "description": "Native Client Executable",
20
+ "__pluginName": "Native Client"
21
+ },
22
+ {
23
+ "type": "application/x-pnacl",
24
+ "suffixes": "",
25
+ "description": "Portable Native Client Executable",
26
+ "__pluginName": "Native Client"
27
+ }
28
+ ],
29
+ "plugins": [
30
+ {
31
+ "name": "Chrome PDF Plugin",
32
+ "filename": "internal-pdf-viewer",
33
+ "description": "Portable Document Format",
34
+ "__mimeTypes": ["application/x-google-chrome-pdf"]
35
+ },
36
+ {
37
+ "name": "Chrome PDF Viewer",
38
+ "filename": "mhjfbmdgcfjbbpaeojofohoefgiehjai",
39
+ "description": "",
40
+ "__mimeTypes": ["application/pdf"]
41
+ },
42
+ {
43
+ "name": "Native Client",
44
+ "filename": "internal-nacl-plugin",
45
+ "description": "",
46
+ "__mimeTypes": ["application/x-nacl", "application/x-pnacl"]
47
+ }
48
+ ]
49
+ }
50
+
51
+
52
+ // That means we're running headful
53
+ const hasPlugins = 'plugins' in navigator && navigator.plugins.length
54
+ if (!(hasPlugins)) {
55
+
56
+ const mimeTypes = generateMagicArray(
57
+ data.mimeTypes,
58
+ MimeTypeArray.prototype,
59
+ MimeType.prototype,
60
+ 'type'
61
+ )
62
+ const plugins = generateMagicArray(
63
+ data.plugins,
64
+ PluginArray.prototype,
65
+ Plugin.prototype,
66
+ 'name'
67
+ )
68
+
69
+ // Plugin and MimeType cross-reference each other, let's do that now
70
+ // Note: We're looping through `data.plugins` here, not the generated `plugins`
71
+ for (const pluginData of data.plugins) {
72
+ pluginData.__mimeTypes.forEach((type, index) => {
73
+ plugins[pluginData.name][index] = mimeTypes[type]
74
+ plugins[type] = mimeTypes[type]
75
+ Object.defineProperty(mimeTypes[type], 'enabledPlugin', {
76
+ value: JSON.parse(JSON.stringify(plugins[pluginData.name])),
77
+ writable: false,
78
+ enumerable: false, // Important: `JSON.stringify(navigator.plugins)`
79
+ configurable: false
80
+ })
81
+ })
82
+ }
83
+
84
+ const patchNavigator = (name, value) =>
85
+ utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
86
+ get() {
87
+ return value
88
+ }
89
+ })
90
+
91
+ patchNavigator('mimeTypes', mimeTypes)
92
+ patchNavigator('plugins', plugins)
93
+ }
94
+ """
TikTok/TikTokApi/stealth/js/navigator_userAgent.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ navigator_userAgent = """
2
+ // replace Headless references in default useragent
3
+ const current_ua = navigator.userAgent
4
+ Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgent', {
5
+ get: () => opts.navigator_user_agent || current_ua.replace('HeadlessChrome/', 'Chrome/')
6
+ })
7
+
8
+ """
TikTok/TikTokApi/stealth/js/navigator_vendor.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ navigator_vendor = """
2
+ Object.defineProperty(Object.getPrototypeOf(navigator), 'vendor', {
3
+ get: () => opts.navigator_vendor || 'Google Inc.',
4
+ })
5
+
6
+ """