Skip to content

Commit d173090

Browse files
committed
ssstik downloader
1 parent 47e52ba commit d173090

File tree

6 files changed

+133
-22
lines changed

6 files changed

+133
-22
lines changed

src/main/java/ru/spliterash/vkVideoUnlocker/tiktok/SnapTikVideoDownloader.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import ru.spliterash.vkVideoUnlocker.common.InfoLoaderService
88
import ru.spliterash.vkVideoUnlocker.common.okHttp.OkHttpFactory
99
import ru.spliterash.vkVideoUnlocker.common.okHttp.executeAsync
1010
import ru.spliterash.vkVideoUnlocker.video.accessor.UrlVideoAccessorImpl
11+
import ru.spliterash.vkVideoUnlocker.video.accessor.VideoAccessor
1112
import java.util.regex.Pattern
1213

1314
// Код деобфускации спизжен отсюда: https://github.com/Ty3uK/snaptik-bot/blob/main/src/url_resolver/snap/util.rs
14-
@Singleton
15+
//@Singleton
1516
class SnapTikVideoDownloader(
1617
okHttpFactory: OkHttpFactory,
1718
private val objectMapper: ObjectMapper,
@@ -26,7 +27,7 @@ class SnapTikVideoDownloader(
2627
private val numberPattern =
2728
Pattern.compile("https://www\\.tiktok\\.com/oembed\\?url=https://www\\.tiktok\\.com/@tiktok/video/(\\d+)")
2829

29-
override suspend fun download(videoUrl: String): TiktokVideo {
30+
override suspend fun download(videoUrl: String): VideoAccessor {
3031
val token = getToken()
3132

3233
val encryptedJs = Request.Builder()
@@ -59,7 +60,6 @@ class SnapTikVideoDownloader(
5960
}
6061
val numberMatcher = numberPattern.matcher(decodedJs)
6162
if (!numberMatcher.find()) throw IllegalStateException("Failed to parse video url from snaptik response")
62-
val number = numberMatcher.group(1)
6363
val hdTokenMatcher = hdTokenPattern.matcher(decodedJs)
6464
if (!hdTokenMatcher.find()) throw IllegalStateException("snaptik stage 4 error: extract hd token, decrypted response: $decodedJs")
6565
val hdToken = hdTokenMatcher.group(1)
@@ -74,7 +74,7 @@ class SnapTikVideoDownloader(
7474
val node = objectMapper.readTree(linkResponse)
7575
val url = node.get("url").asText()
7676

77-
return TiktokVideo(number, UrlVideoAccessorImpl(infoLoaderService, client, url))
77+
return UrlVideoAccessorImpl(infoLoaderService, client, url)
7878
}
7979

8080
private suspend fun getToken(): String {
@@ -83,12 +83,12 @@ class SnapTikVideoDownloader(
8383
.get()
8484
.build()
8585
.executeAsync(client)
86-
.body
87-
.string()
86+
.use { it.body.string() }
8887
val matcher = tokenPattern.matcher(response)
8988
if (!matcher.find()) throw IllegalStateException("snaptik stage 1 error: token extract")
9089

9190
return matcher.group(1)
91+
9292
}
9393

9494

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
}

src/main/java/ru/spliterash/vkVideoUnlocker/tiktok/TiktokChain.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ class TiktokChain(
4242
}
4343
editableMessage.sendOrUpdate("Начинаем обработку tiktok контента, это может быть долго")
4444
val contentType = directUrlMatcher.group("type")
45+
val tiktokVideoId = directUrlMatcher.group("id")
4546
if (contentType == "video") {
46-
val id = tiktokService.getVkId(videoUrl, MessageNotificationProgressMeter(editableMessage))
47+
val id = tiktokService.getVkId(videoUrl, tiktokVideoId, MessageNotificationProgressMeter(editableMessage))
4748
editableMessage.sendOrUpdate(attachments = "video$id")
4849
} else if (contentType == "photo") {
4950
val (musicAttachment, attachmentIds) = tiktokService.getPhotoAttachmentIds(message.peerId, videoUrl)

src/main/java/ru/spliterash/vkVideoUnlocker/tiktok/TiktokService.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,14 @@ class TiktokService(
2929
private val tiktokPhotoDownloader: TiktokPhotoDownloader,
3030
private val fFmpegService: FFmpegService,
3131
) {
32-
suspend fun getVkId(tiktokVideoUrl: String, progressMeter: ProgressMeter): String {
33-
val info = tiktokVideoDownloader.download(tiktokVideoUrl)
34-
val video = tiktokVideoRepository.findVideo(info.id)
32+
suspend fun getVkId(tiktokVideoUrl: String, tiktokVideoId: String, progressMeter: ProgressMeter): String {
33+
val video = tiktokVideoRepository.findVideo(tiktokVideoId)
3534
if (video != null) return video.vkId
35+
val accessor = tiktokVideoDownloader.download(tiktokVideoUrl)
36+
val vkId = reUpload(tiktokVideoId, accessor, progressMeter)
3637

37-
val vkId = reUpload(info.id, info.accessor, progressMeter)
38-
39-
tiktokVideoRepository.save(TiktokVideoEntity(info.id, vkId))
4038

39+
tiktokVideoRepository.save(TiktokVideoEntity(tiktokVideoId, vkId))
4140
return vkId
4241
}
4342

src/main/java/ru/spliterash/vkVideoUnlocker/tiktok/TiktokVideo.kt

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package ru.spliterash.vkVideoUnlocker.tiktok
22

3+
import ru.spliterash.vkVideoUnlocker.video.accessor.VideoAccessor
4+
35

46
interface TiktokVideoDownloader {
5-
suspend fun download(videoUrl: String): TiktokVideo
7+
suspend fun download(videoUrl: String): VideoAccessor
68
}

0 commit comments

Comments
 (0)