Fix issue#2 by replacing \u0026 with & character as the url is encoded
This commit is contained in:
parent
b256cb9bf2
commit
0d5e1a73d2
5 changed files with 48 additions and 11 deletions
12
app/src/main/java/org/fnives/tiktokdownloader/Logger.kt
Normal file
12
app/src/main/java/org/fnives/tiktokdownloader/Logger.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.fnives.tiktokdownloader
|
||||||
|
|
||||||
|
object Logger {
|
||||||
|
|
||||||
|
private const val TAG = "TTDTag"
|
||||||
|
|
||||||
|
fun logMessage(message: String) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
System.err.println("TTDTag $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package org.fnives.tiktokdownloader.data.network
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.fnives.tiktokdownloader.Logger
|
||||||
import org.fnives.tiktokdownloader.data.model.VideoInPending
|
import org.fnives.tiktokdownloader.data.model.VideoInPending
|
||||||
import org.fnives.tiktokdownloader.data.model.VideoInSavingIntoFile
|
import org.fnives.tiktokdownloader.data.model.VideoInSavingIntoFile
|
||||||
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
|
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
|
||||||
|
|
@ -22,8 +23,10 @@ class TikTokDownloadRemoteSource(
|
||||||
wrapIntoProperException {
|
wrapIntoProperException {
|
||||||
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
||||||
val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url)
|
val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url)
|
||||||
|
Logger.logMessage("actualUrl found = ${actualUrl.url}")
|
||||||
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
||||||
val videoUrl = service.getVideoUrl(actualUrl.url)
|
val videoUrl = service.getVideoUrl(actualUrl.url)
|
||||||
|
Logger.logMessage("videoFileUrl found = ${videoUrl.videoFileUrl}")
|
||||||
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
delay(delayBeforeRequest) // added just so captcha trigger may not happen
|
||||||
val response = service.getVideo(videoUrl.videoFileUrl)
|
val response = service.getVideo(videoUrl.videoFileUrl)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package org.fnives.tiktokdownloader.data.network.parsing.converter
|
package org.fnives.tiktokdownloader.data.network.parsing.converter
|
||||||
|
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
|
import org.fnives.tiktokdownloader.Logger
|
||||||
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
|
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
|
||||||
import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl
|
import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl
|
||||||
import kotlin.jvm.Throws
|
|
||||||
|
|
||||||
class VideoFileUrlConverter(
|
class VideoFileUrlConverter(
|
||||||
private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse
|
private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse
|
||||||
|
|
@ -12,8 +12,8 @@ class VideoFileUrlConverter(
|
||||||
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class)
|
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class)
|
||||||
override fun convertSafely(responseBody: ResponseBody): VideoFileUrl? {
|
override fun convertSafely(responseBody: ResponseBody): VideoFileUrl? {
|
||||||
val html = responseBody.string().also(throwIfIsCaptchaResponse::invoke)
|
val html = responseBody.string().also(throwIfIsCaptchaResponse::invoke)
|
||||||
val url = tryToParseDownloadLink(html)
|
val url = tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") }
|
||||||
?: tryToParseVideoSrc(html)
|
?: tryToParseVideoSrc(html).also { Logger.logMessage("parsed video src = $it") }
|
||||||
?: throw IllegalArgumentException("Couldn't parse url from HTML: $html")
|
?: throw IllegalArgumentException("Couldn't parse url from HTML: $html")
|
||||||
|
|
||||||
return VideoFileUrl(url)
|
return VideoFileUrl(url)
|
||||||
|
|
@ -26,7 +26,7 @@ class VideoFileUrlConverter(
|
||||||
html.split("\"playAddr\"")[1]
|
html.split("\"playAddr\"")[1]
|
||||||
.dropWhile { it != '\"' }.drop(1)
|
.dropWhile { it != '\"' }.drop(1)
|
||||||
.takeWhile { it != '\"' }
|
.takeWhile { it != '\"' }
|
||||||
.replace("\\u0026", "&")
|
.urlCharacterReplacements()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -39,10 +39,19 @@ class VideoFileUrlConverter(
|
||||||
.dropWhile { it != '=' }
|
.dropWhile { it != '=' }
|
||||||
.dropWhile { it != '\"' }.drop(1)
|
.dropWhile { it != '\"' }.drop(1)
|
||||||
.takeWhile { it != '\"' }
|
.takeWhile { it != '\"' }
|
||||||
.replace("\\u0026", "&")
|
.urlCharacterReplacements()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val replacements = mutableMapOf(
|
||||||
|
"\\u002F" to "/",
|
||||||
|
"\\u0026" to "&"
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String.urlCharacterReplacements(): String =
|
||||||
|
replacements.entries.fold(this) { result, toReplaceEntry ->
|
||||||
|
result.replace(toReplaceEntry.key, toReplaceEntry.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,14 +128,19 @@ class TikTokDownloadRemoteSourceTest {
|
||||||
mockWebServer.enqueue(MockResponse().setResponseCode(200).setHeader("Content-Type", "video/mp4").setBody("banan"))
|
mockWebServer.enqueue(MockResponse().setResponseCode(200).setHeader("Content-Type", "video/mp4").setBody("banan"))
|
||||||
val videoInPending = VideoInPending("alma", TEST_URL)
|
val videoInPending = VideoInPending("alma", TEST_URL)
|
||||||
|
|
||||||
|
|
||||||
val response = sut.getVideo(videoInPending)
|
val response = sut.getVideo(videoInPending)
|
||||||
Assertions.assertEquals(expectedId, response.id)
|
Assertions.assertEquals(expectedId, response.id)
|
||||||
Assertions.assertEquals(expectedUrl, response.url)
|
Assertions.assertEquals(expectedUrl, response.url)
|
||||||
Assertions.assertEquals(expectedContentType, response.contentType)
|
Assertions.assertEquals(expectedContentType, response.contentType)
|
||||||
Assertions.assertEquals("banan", response.byteStream.reader().readText())
|
Assertions.assertEquals("banan", response.byteStream.reader().readText())
|
||||||
|
|
||||||
|
mockWebServer.takeRequest()
|
||||||
|
mockWebServer.takeRequest()
|
||||||
|
val videoRequest = mockWebServer.takeRequest()
|
||||||
|
Assertions.assertEquals("http://localhost:8080/?a=a&b=b&c=c", videoRequest.requestUrl?.toUri()?.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_proper_responses_as_variant2_THEN_parsed_properly() = runBlocking<Unit> {
|
fun GIVEN_proper_responses_as_variant2_THEN_parsed_properly() = runBlocking<Unit> {
|
||||||
val expectedId = "e-alma"
|
val expectedId = "e-alma"
|
||||||
|
|
@ -154,6 +159,10 @@ class TikTokDownloadRemoteSourceTest {
|
||||||
Assertions.assertEquals(expectedUrl, response.url)
|
Assertions.assertEquals(expectedUrl, response.url)
|
||||||
Assertions.assertEquals(expectedContentType, response.contentType)
|
Assertions.assertEquals(expectedContentType, response.contentType)
|
||||||
Assertions.assertEquals("a-banan", response.byteStream.reader().readText())
|
Assertions.assertEquals("a-banan", response.byteStream.reader().readText())
|
||||||
|
mockWebServer.takeRequest()
|
||||||
|
mockWebServer.takeRequest()
|
||||||
|
val videoRequest = mockWebServer.takeRequest()
|
||||||
|
Assertions.assertEquals("http://localhost:8080/?a=a&b=b&c=c", videoRequest.requestUrl?.toUri()?.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -259,31 +268,34 @@ class TikTokDownloadRemoteSourceTest {
|
||||||
private const val MAIN_PAGE_VARIANT_2_RESPONSE = "response/main_page_v1.html"
|
private const val MAIN_PAGE_VARIANT_2_RESPONSE = "response/main_page_v1.html"
|
||||||
private const val PORT = 8080
|
private const val PORT = 8080
|
||||||
private const val TEST_URL = "http://127.0.0.1:$PORT"
|
private const val TEST_URL = "http://127.0.0.1:$PORT"
|
||||||
|
private const val SHORTENED_TEST_URL = "http://127.0.0.1:$PORT"
|
||||||
|
private const val VIDEO_FILE_TEST_URL = "http:\\u002F/127.0.0.1:$PORT?a=a\\u0026b=b&c=c"
|
||||||
|
private const val CAPTCHA_TEST_URL = "http://127.0.0.1:$PORT"
|
||||||
|
|
||||||
private fun Any.readResourceFileShortenedUrlResponse() =
|
private fun Any.readResourceFileShortenedUrlResponse() =
|
||||||
readResourceFile(SHORTENED_URL_RESPONSE)
|
readResourceFile(SHORTENED_URL_RESPONSE)
|
||||||
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", TEST_URL)
|
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", SHORTENED_TEST_URL)
|
||||||
|
|
||||||
private fun Any.readResourceFileMainPageVariant1Response() =
|
private fun Any.readResourceFileMainPageVariant1Response() =
|
||||||
readResourceFile(MAIN_PAGE_VARIANT_1_RESPONSE)
|
readResourceFile(MAIN_PAGE_VARIANT_1_RESPONSE)
|
||||||
.replace(
|
.replace(
|
||||||
"https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=",
|
"https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=",
|
||||||
TEST_URL
|
VIDEO_FILE_TEST_URL
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Any.readResourceFileMainPageVariant2Response() =
|
private fun Any.readResourceFileMainPageVariant2Response() =
|
||||||
readResourceFile(MAIN_PAGE_VARIANT_2_RESPONSE)
|
readResourceFile(MAIN_PAGE_VARIANT_2_RESPONSE)
|
||||||
.replace(
|
.replace(
|
||||||
"https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=",
|
"https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=",
|
||||||
TEST_URL
|
VIDEO_FILE_TEST_URL
|
||||||
)
|
)
|
||||||
private fun Any.readCaptchaOneResponse() =
|
private fun Any.readCaptchaOneResponse() =
|
||||||
readResourceFile(CAPTCHA_REQUIRED_RESPONSE_ONE)
|
readResourceFile(CAPTCHA_REQUIRED_RESPONSE_ONE)
|
||||||
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", TEST_URL)
|
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", CAPTCHA_TEST_URL)
|
||||||
|
|
||||||
private fun Any.readCaptchaTwoResponse() =
|
private fun Any.readCaptchaTwoResponse() =
|
||||||
readResourceFile(CAPTCHA_REQUIRED_RESPONSE_TWO)
|
readResourceFile(CAPTCHA_REQUIRED_RESPONSE_TWO)
|
||||||
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", TEST_URL)
|
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", CAPTCHA_TEST_URL)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private fun captchaResponses() = Stream.of(
|
private fun captchaResponses() = Stream.of(
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ class TikTokDownloadRemoteSourceUpToDateTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled("Can trigger captcha, so only run it separately")
|
@Disabled("Can trigger captcha, so only run it separately")
|
||||||
|
@Timeout(value = 120)
|
||||||
@Test
|
@Test
|
||||||
fun GIVEN_actualVideo_WHEN_downloading_THEN_the_file_matching_with_the_previously_loaded_video() {
|
fun GIVEN_actualVideo_WHEN_downloading_THEN_the_file_matching_with_the_previously_loaded_video() {
|
||||||
val parameter = VideoInPending("123", SUBJECT_VIDEO_URL)
|
val parameter = VideoInPending("123", SUBJECT_VIDEO_URL)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue