Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +10 -0
- .gradio/certificate.pem +31 -0
- Data/JSON/TotalView.json +1 -0
- Data/JSON/blackList.json +9 -0
- Data/JSON/index.json +1 -0
- Data/TXT/Cacto0o.txt +14 -0
- Data/TXT/T2x2.txt +48 -0
- Html/index.html +104 -0
- Html/main.js +154 -0
- Html/reconnecting-websoket.min.js +365 -0
- Main.py +222 -0
- README.md +1 -7
- TikTok/Cookies/__init__.py +0 -0
- TikTok/Cookies/cookie.py +94 -0
- TikTok/Server/SaveTotalView.py +52 -0
- TikTok/Server/__init__.py +0 -0
- TikTok/Server/main.py +124 -0
- TikTok/Server/users.py +5 -0
- TikTok/Statistic/AsyncUser.py +183 -0
- TikTok/Statistic/SingleUser.py +179 -0
- TikTok/Statistic/__init__.py +0 -0
- TikTok/Statistic/tiktok.py +170 -0
- TikTok/TikTokApi/__init__.py +1 -0
- TikTok/TikTokApi/api/__init__.py +0 -0
- TikTok/TikTokApi/api/comment.py +92 -0
- TikTok/TikTokApi/api/hashtag.py +167 -0
- TikTok/TikTokApi/api/search.py +106 -0
- TikTok/TikTokApi/api/sound.py +179 -0
- TikTok/TikTokApi/api/trending.py +60 -0
- TikTok/TikTokApi/api/user.py +280 -0
- TikTok/TikTokApi/api/video.py +332 -0
- TikTok/TikTokApi/exceptions.py +35 -0
- TikTok/TikTokApi/helpers.py +36 -0
- TikTok/TikTokApi/stealth/__init__.py +1 -0
- TikTok/TikTokApi/stealth/js/__init__.py +0 -0
- TikTok/TikTokApi/stealth/js/chrome_app.py +73 -0
- TikTok/TikTokApi/stealth/js/chrome_csi.py +29 -0
- TikTok/TikTokApi/stealth/js/chrome_hairline.py +16 -0
- TikTok/TikTokApi/stealth/js/chrome_load_times.py +124 -0
- TikTok/TikTokApi/stealth/js/chrome_runtime.py +265 -0
- TikTok/TikTokApi/stealth/js/generate_magic_arrays.py +144 -0
- TikTok/TikTokApi/stealth/js/iframe_contentWindow.py +99 -0
- TikTok/TikTokApi/stealth/js/media_codecs.py +65 -0
- TikTok/TikTokApi/stealth/js/navigator_hardwareConcurrency.py +10 -0
- TikTok/TikTokApi/stealth/js/navigator_languages.py +6 -0
- TikTok/TikTokApi/stealth/js/navigator_permissions.py +22 -0
- TikTok/TikTokApi/stealth/js/navigator_platform.py +7 -0
- TikTok/TikTokApi/stealth/js/navigator_plugins.py +94 -0
- TikTok/TikTokApi/stealth/js/navigator_userAgent.py +8 -0
- 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 |
-
|
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 |
+
"""
|