1+ package ru.spliterash.vkVideoUnlocker.tiktok
2+
3+ import com.fasterxml.jackson.databind.ObjectMapper
4+ import jakarta.inject.Singleton
5+ import okhttp3.FormBody
6+ import okhttp3.Request
7+ import org.jsoup.Jsoup
8+ import ru.spliterash.vkVideoUnlocker.common.InfoLoaderService
9+ import ru.spliterash.vkVideoUnlocker.common.okHttp.OkHttpFactory
10+ import ru.spliterash.vkVideoUnlocker.common.okHttp.executeAsync
11+ import ru.spliterash.vkVideoUnlocker.video.accessor.UrlVideoAccessorImpl
12+ import ru.spliterash.vkVideoUnlocker.video.accessor.VideoAccessor
13+ import java.util.regex.Pattern
14+
15+ @Singleton
16+ class SssTikVideoDownloader (
17+ okHttpFactory : OkHttpFactory ,
18+ private val objectMapper : ObjectMapper ,
19+ private val infoLoaderService : InfoLoaderService
20+ ) : TiktokVideoDownloader {
21+ private val client = okHttpFactory.create()
22+ .build()
23+
24+ private val ttPattern = Pattern .compile(" s_tt = '(?<tt>.*?)'" )
25+ private val onClickPattern = Pattern .compile(" downloadX\\ ('(.*?)'\\ )" )
26+
27+ override suspend fun download (videoUrl : String ): VideoAccessor {
28+ val tt = getTt()
29+ val downloadXUrl = acquireDownloadXUrl(videoUrl, tt)
30+ val downloadUrl = transformXDownload(downloadXUrl)
31+
32+ return UrlVideoAccessorImpl (infoLoaderService, client, downloadUrl)
33+ }
34+
35+ private suspend fun getTt (): String {
36+ val response = Request .Builder ()
37+ .url(" https://ssstik.io/" )
38+ .get()
39+ .build()
40+ .executeAsync(client)
41+ .use { it.body.string() }
42+
43+ val matcher = ttPattern.matcher(response)
44+ if (! matcher.find()) throw IllegalStateException (" Ssstik error 1: fail to parse tt" )
45+
46+ return matcher.group(" tt" )
47+ }
48+
49+ private suspend fun acquireDownloadXUrl (url : String , tt : String ): DownloadXUrlResult {
50+ val request = Request .Builder ()
51+ .url(" https://ssstik.io/abc?url=dl" )
52+ .post(
53+ FormBody .Builder ()
54+ .add(" id" , url)
55+ .add(" locale" , " en" )
56+ .add(" tt" , tt)
57+ .build()
58+ )
59+ .setFirefox()
60+ .build()
61+ val response = request
62+ .executeAsync(client)
63+ .use { it.body.string() }
64+
65+ if (response.isBlank()) throw IllegalStateException (" Ssstik error 2: empty response" )
66+ val parsed = Jsoup .parse(response)
67+ val hdButton = parsed.select(" #hd_download" ).first()
68+ ? : throw IllegalStateException (" Ssstik error 3: no hd_download element found" )
69+
70+ val onClick = hdButton.attr(" onclick" )
71+ val matcher = onClickPattern.matcher(onClick)
72+ if (! matcher.find()) throw IllegalStateException (" Ssstik error 4: invalid onClick content: $onClick " )
73+
74+ val xUrl = " https://ssstik.io" + matcher.group(1 )
75+ val input = parsed.select(" input[name=tt]" ).first()
76+ ? : throw IllegalStateException (" Sstik error 5: no hidden tt input found" )
77+
78+ return DownloadXUrlResult (
79+ xUrl,
80+ input.attr(" value" )
81+ )
82+
83+ }
84+
85+ private data class DownloadXUrlResult (
86+ val url : String ,
87+ val newTT : String
88+ )
89+
90+ private suspend fun transformXDownload (downloadXUrl : DownloadXUrlResult ): String {
91+ val finalUrl = Request
92+ .Builder ()
93+ .url(downloadXUrl.url)
94+ .setFirefox()
95+ .post(
96+ FormBody .Builder ()
97+ .add(" tt" , downloadXUrl.newTT)
98+ .build()
99+ )
100+ .header(" HX-Request" , " true" )
101+ .header(" HX-Trigger" , " hd_download" )
102+ .header(" HX-Target" , " hd_download" )
103+ .header(" HX-Current-URL" , " https://ssstik.io/" )
104+ .build()
105+ .executeAsync(client)
106+ .use { it.headers[" hx-redirect" ] }
107+
108+ if (finalUrl == null ) throw IllegalStateException (" Ssstik error 6: no hx-redirect header found" )
109+
110+
111+ return finalUrl
112+ }
113+
114+
115+ private fun Request.Builder.setFirefox () =
116+ addHeader(" user-agent" , " Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0" )
117+ }
0 commit comments