From b1e44dce438aaecbb00d2c3a9f4d30b53e25167a Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Tue, 13 May 2025 18:01:27 +0300 Subject: [PATCH] #17 Show specific error if video is deleted --- .../data/model/ProcessState.kt | 13 +- .../network/TikTokDownloadRemoteSource.kt | 63 +- .../exceptions/CaptchaRequiredException.kt | 5 +- .../data/network/exceptions/HtmlException.kt | 5 + .../network/exceptions/NetworkException.kt | 5 +- .../network/exceptions/ParsingException.kt | 5 +- .../exceptions/VideoDeletedException.kt | 4 + .../parsing/TikTokWebPageConverterFactory.kt | 14 +- .../converter/ActualVideoPageUrlConverter.kt | 4 +- .../ParsingExceptionThrowingConverter.kt | 13 +- .../converter/ThrowIfIsCaptchaResponse.kt | 5 +- .../ThrowIfVideoIsDeletedResponse.kt | 13 + .../converter/VideoFileUrlConverter.kt | 4 +- .../VideoDownloadingProcessorUseCase.kt | 18 +- .../di/module/NetworkModule.kt | 8 +- .../tiktokdownloader/ui/main/MainActivity.kt | 1 + .../tiktokdownloader/ui/main/MainViewModel.kt | 4 +- .../ui/service/QueueServiceViewModel.kt | 2 + app/src/main/res/values/strings.xml | 1 + .../VideoDownloadingProcessorUseCaseTest.kt | 20 +- .../TikTokDownloadRemoteSourceUpToDateTest.kt | 24 +- .../resources/response/deleted_video.html | 5779 +++++++++++++++++ 22 files changed, 5945 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt create mode 100644 app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt create mode 100644 app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfVideoIsDeletedResponse.kt create mode 100644 app/src/test/resources/response/deleted_video.html diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/model/ProcessState.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/model/ProcessState.kt index 8d8cea6..bb6489f 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/model/ProcessState.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/model/ProcessState.kt @@ -4,10 +4,11 @@ sealed class ProcessState { data class Processing(val videoInPending: VideoInPending) : ProcessState() data class Processed(val videoDownloaded: VideoDownloaded) : ProcessState() - object NetworkError : ProcessState() - object ParsingError : ProcessState() - object CaptchaError : ProcessState() - object UnknownError : ProcessState() - object StorageError : ProcessState() - object Finished: ProcessState() + data object NetworkError : ProcessState() + data object ParsingError : ProcessState() + data object VideoDeletedError : ProcessState() + data object CaptchaError : ProcessState() + data object UnknownError : ProcessState() + data object StorageError : ProcessState() + data object Finished: ProcessState() } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt index 79084a6..e0ab21e 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/TikTokDownloadRemoteSource.kt @@ -7,8 +7,10 @@ import org.fnives.tiktokdownloader.Logger import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInSavingIntoFile import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException +import org.fnives.tiktokdownloader.data.network.exceptions.HtmlException import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException +import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException import org.fnives.tiktokdownloader.data.network.parsing.converter.VideoFileUrlConverter import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl import org.fnives.tiktokdownloader.data.network.session.CookieStore @@ -21,36 +23,42 @@ class TikTokDownloadRemoteSource( ) { @Throws(ParsingException::class, NetworkException::class, CaptchaRequiredException::class) - suspend fun getVideo(videoInPending: VideoInPending): VideoInSavingIntoFile = withContext(Dispatchers.IO) { - cookieStore.clear() - wrapIntoProperException { - delay(delayBeforeRequest) // added just so captcha trigger may not happen - val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url) - val videoUrl: VideoFileUrl - if (actualUrl.url != null) { - Logger.logMessage("actualUrl found = ${actualUrl.url}") + suspend fun getVideo(videoInPending: VideoInPending): VideoInSavingIntoFile = + withContext(Dispatchers.IO) { + cookieStore.clear() + wrapIntoProperException { delay(delayBeforeRequest) // added just so captcha trigger may not happen + val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url) + val videoUrl: VideoFileUrl + if (actualUrl.url != null) { + Logger.logMessage("actualUrl found = ${actualUrl.url}") + delay(delayBeforeRequest) // added just so captcha trigger may not happen - videoUrl = service.getVideoUrl(actualUrl.url) - } else { - Logger.logMessage("actualUrl not found. Attempting to parse videoUrl") + videoUrl = service.getVideoUrl(actualUrl.url) + } else { + Logger.logMessage("actualUrl not found. Attempting to parse videoUrl") - videoUrl = videoFileUrlConverter.convertSafely(actualUrl.fullResponse) + videoUrl = videoFileUrlConverter.convertSafely(actualUrl.fullResponse) + } + Logger.logMessage("videoFileUrl found = ${videoUrl.videoFileUrl}") + delay(delayBeforeRequest) // added just so captcha trigger may not happen + val response = service.getVideo(videoUrl.videoFileUrl) + + VideoInSavingIntoFile( + id = videoInPending.id, + url = videoInPending.url, + contentType = response.mediaType?.let { + VideoInSavingIntoFile.ContentType( + it.type, + it.subtype + ) + }, + byteStream = response.videoInputStream + ) } - Logger.logMessage("videoFileUrl found = ${videoUrl.videoFileUrl}") - delay(delayBeforeRequest) // added just so captcha trigger may not happen - val response = service.getVideo(videoUrl.videoFileUrl) - - VideoInSavingIntoFile( - id = videoInPending.id, - url = videoInPending.url, - contentType = response.mediaType?.let { VideoInSavingIntoFile.ContentType(it.type, it.subtype) }, - byteStream = response.videoInputStream - ) } - } - @Throws(ParsingException::class, NetworkException::class) + @Throws(ParsingException::class, NetworkException::class, VideoDeletedException::class) private suspend fun wrapIntoProperException(request: suspend () -> T): T = try { request() @@ -58,7 +66,12 @@ class TikTokDownloadRemoteSource( throw parsingException } catch (captchaRequiredException: CaptchaRequiredException) { throw captchaRequiredException + } catch (videoDeletedException: VideoDeletedException) { + throw videoDeletedException } catch (throwable: Throwable) { - throw NetworkException(cause = throwable) + throw NetworkException( + cause = throwable, + html = (throwable as? HtmlException)?.html.orEmpty() + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt index 98348a6..735f0e8 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/CaptchaRequiredException.kt @@ -1,3 +1,6 @@ package org.fnives.tiktokdownloader.data.network.exceptions -class CaptchaRequiredException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) \ No newline at end of file +class CaptchaRequiredException( + message: String? = null, cause: Throwable? = null, + override val html: String, +) : Throwable(message, cause), HtmlException \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt new file mode 100644 index 0000000..8e9546a --- /dev/null +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/HtmlException.kt @@ -0,0 +1,5 @@ +package org.fnives.tiktokdownloader.data.network.exceptions + +interface HtmlException { + val html: String +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt index 4187df9..2298c76 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/NetworkException.kt @@ -1,3 +1,6 @@ package org.fnives.tiktokdownloader.data.network.exceptions -class NetworkException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) \ No newline at end of file +class NetworkException( + message: String? = null, cause: Throwable? = null, + override val html: String, +) : Throwable(message, cause), HtmlException \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt index 8e23218..94166be 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/ParsingException.kt @@ -2,4 +2,7 @@ package org.fnives.tiktokdownloader.data.network.exceptions import java.io.IOException -class ParsingException(message: String? = null, cause: Throwable? = null) : IOException(message, cause) \ No newline at end of file +class ParsingException( + message: String? = null, cause: Throwable? = null, + override val html: String, +) : IOException(message, cause), HtmlException \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt new file mode 100644 index 0000000..ad510c8 --- /dev/null +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/exceptions/VideoDeletedException.kt @@ -0,0 +1,4 @@ +package org.fnives.tiktokdownloader.data.network.exceptions + +class VideoDeletedException(override val html: String) : Throwable(), + HtmlException \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/TikTokWebPageConverterFactory.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/TikTokWebPageConverterFactory.kt index caed6c5..3d47d77 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/TikTokWebPageConverterFactory.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/TikTokWebPageConverterFactory.kt @@ -3,6 +3,7 @@ package org.fnives.tiktokdownloader.data.network.parsing import okhttp3.ResponseBody import org.fnives.tiktokdownloader.data.network.parsing.converter.ActualVideoPageUrlConverter import org.fnives.tiktokdownloader.data.network.parsing.converter.ThrowIfIsCaptchaResponse +import org.fnives.tiktokdownloader.data.network.parsing.converter.ThrowIfVideoIsDeletedResponse import org.fnives.tiktokdownloader.data.network.parsing.converter.VideoFileUrlConverter import org.fnives.tiktokdownloader.data.network.parsing.converter.VideoResponseConverter import org.fnives.tiktokdownloader.data.network.parsing.response.ActualVideoPageUrl @@ -12,7 +13,10 @@ import retrofit2.Converter import retrofit2.Retrofit import java.lang.reflect.Type -class TikTokWebPageConverterFactory(private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse) : Converter.Factory() { +class TikTokWebPageConverterFactory( + private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse, + private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse +) : Converter.Factory() { override fun responseBodyConverter( type: Type, @@ -20,8 +24,12 @@ class TikTokWebPageConverterFactory(private val throwIfIsCaptchaResponse: ThrowI retrofit: Retrofit ): Converter? = when (type) { - ActualVideoPageUrl::class.java -> ActualVideoPageUrlConverter(throwIfIsCaptchaResponse) - VideoFileUrl::class.java -> VideoFileUrlConverter(throwIfIsCaptchaResponse) + ActualVideoPageUrl::class.java -> ActualVideoPageUrlConverter( + throwIfIsCaptchaResponse, + throwIfVideoIsDeletedResponse + ) + + VideoFileUrl::class.java -> VideoFileUrlConverter(throwIfIsCaptchaResponse, throwIfVideoIsDeletedResponse) VideoResponse::class.java -> VideoResponseConverter() else -> super.responseBodyConverter(type, annotations, retrofit) } diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt index 68ac127..8c04a94 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ActualVideoPageUrlConverter.kt @@ -6,7 +6,8 @@ import org.fnives.tiktokdownloader.data.network.parsing.response.ActualVideoPage import kotlin.jvm.Throws class ActualVideoPageUrlConverter( - private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse + private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse, + private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse ) : ParsingExceptionThrowingConverter() { @Throws(IndexOutOfBoundsException::class, CaptchaRequiredException::class) @@ -15,6 +16,7 @@ class ActualVideoPageUrlConverter( return try { val actualVideoPageUrl = responseBodyAsString .also(throwIfIsCaptchaResponse::invoke) + .also(throwIfVideoIsDeletedResponse::invoke) .split("rel=\"canonical\" href=\"")[1] .split("\"")[0] diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ParsingExceptionThrowingConverter.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ParsingExceptionThrowingConverter.kt index c9eac15..8fd27da 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ParsingExceptionThrowingConverter.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ParsingExceptionThrowingConverter.kt @@ -2,25 +2,32 @@ package org.fnives.tiktokdownloader.data.network.parsing.converter import okhttp3.ResponseBody import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException +import org.fnives.tiktokdownloader.data.network.exceptions.HtmlException import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException +import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException import retrofit2.Converter abstract class ParsingExceptionThrowingConverter : Converter { - @Throws(ParsingException::class, CaptchaRequiredException::class) + @Throws(ParsingException::class, CaptchaRequiredException::class, VideoDeletedException::class) final override fun convert(value: ResponseBody): T? = doActionSafely { convertSafely(value) } - @Throws(ParsingException::class, CaptchaRequiredException::class) + @Throws(ParsingException::class, CaptchaRequiredException::class, VideoDeletedException::class) fun doActionSafely(action: () -> T): T { try { return action() } catch (captchaRequiredException: CaptchaRequiredException) { throw captchaRequiredException + } catch(videoDeletedException: VideoDeletedException) { + throw videoDeletedException } catch (throwable: Throwable) { - throw ParsingException(cause = throwable) + throw ParsingException( + cause = throwable, + html = (throwable as? HtmlException)?.html.orEmpty() + ) } } diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfIsCaptchaResponse.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfIsCaptchaResponse.kt index 0d80052..c1cbc60 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfIsCaptchaResponse.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfIsCaptchaResponse.kt @@ -1,16 +1,15 @@ package org.fnives.tiktokdownloader.data.network.parsing.converter import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException -import kotlin.jvm.Throws class ThrowIfIsCaptchaResponse { @Throws(CaptchaRequiredException::class) fun invoke(html: String) { if (html.isEmpty()) { - throw CaptchaRequiredException("Empty body") + throw CaptchaRequiredException("Empty body", html = html) } else if (html.contains("captcha.js")) { - throw CaptchaRequiredException("Contains Captcha keyword") + throw CaptchaRequiredException("Contains Captcha keyword", html = html) } } } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfVideoIsDeletedResponse.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfVideoIsDeletedResponse.kt new file mode 100644 index 0000000..820f96c --- /dev/null +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/ThrowIfVideoIsDeletedResponse.kt @@ -0,0 +1,13 @@ +package org.fnives.tiktokdownloader.data.network.parsing.converter + +import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException + +class ThrowIfVideoIsDeletedResponse { + + @Throws(VideoDeletedException::class) + fun invoke(html: String) { + if (html.contains("\"statusMsg\":\"status_deleted\"")) { + throw VideoDeletedException(html = html) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt index 93210c3..f337798 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/network/parsing/converter/VideoFileUrlConverter.kt @@ -7,7 +7,8 @@ import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl class VideoFileUrlConverter( - private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse + private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse, + private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse, ) : ParsingExceptionThrowingConverter() { @Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class) @@ -23,6 +24,7 @@ class VideoFileUrlConverter( @Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class) private fun convert(responseBody: String): VideoFileUrl { val html = responseBody.also(throwIfIsCaptchaResponse::invoke) + .also(throwIfVideoIsDeletedResponse::invoke) val url = tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") } ?: tryToParseVideoSrc(html).also { Logger.logMessage("parsed video src = $it") } ?: throw IllegalArgumentException("Couldn't parse url from HTML: $html") diff --git a/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCase.kt b/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCase.kt index 155df97..fb8e2a6 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCase.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCase.kt @@ -2,6 +2,7 @@ package org.fnives.tiktokdownloader.data.usecase import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +31,7 @@ import org.fnives.tiktokdownloader.data.network.TikTokDownloadRemoteSource import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException +import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException @OptIn(FlowPreview::class) class VideoDownloadingProcessorUseCase( @@ -42,6 +44,8 @@ class VideoDownloadingProcessorUseCase( ) { private val fetch = MutableStateFlow(ProcessingState.RUNNING) + + @OptIn(ExperimentalCoroutinesApi::class) private val _processState by lazy { combineIntoPair(fetch, videoInPendingLocalSource.observeFirstPendingVideo()) .filter { it.first == ProcessingState.RUNNING } @@ -84,13 +88,17 @@ class VideoDownloadingProcessorUseCase( videoInPendingLocalSource.removeVideoFromQueue(videoInPending) alreadyDownloaded } + captchaTimeoutLocalSource.isInCaptchaTimeout() -> { - throw CaptchaRequiredException("In Captcha Timeout!") + throw CaptchaRequiredException("In Captcha Timeout!", html = "") } + else -> { videoInProgressLocalSource.markVideoAsInProgress(videoInPending) - val videoInSavingIntoFile: VideoInSavingIntoFile = tikTokDownloadRemoteSource.getVideo(videoInPending) - val videoDownloaded: VideoDownloaded = videoDownloadedLocalSource.saveVideo(videoInSavingIntoFile) + val videoInSavingIntoFile: VideoInSavingIntoFile = + tikTokDownloadRemoteSource.getVideo(videoInPending) + val videoDownloaded: VideoDownloaded = + videoDownloadedLocalSource.saveVideo(videoInSavingIntoFile) videoInPendingLocalSource.removeVideoFromQueue(videoInPending) videoDownloaded @@ -102,6 +110,8 @@ class VideoDownloadingProcessorUseCase( ProcessState.NetworkError } catch (parsingException: ParsingException) { ProcessState.ParsingError + } catch (videoDeletedException: VideoDeletedException) { + ProcessState.VideoDeletedError } catch (storageException: StorageException) { ProcessState.StorageError } catch (captchaRequiredException: CaptchaRequiredException) { @@ -124,10 +134,12 @@ class VideoDownloadingProcessorUseCase( is ProcessState.Processing, is ProcessState.Processed, ProcessState.Finished -> false + ProcessState.NetworkError, ProcessState.ParsingError, ProcessState.StorageError, ProcessState.UnknownError, + ProcessState.VideoDeletedError, ProcessState.CaptchaError -> true } diff --git a/app/src/main/java/org/fnives/tiktokdownloader/di/module/NetworkModule.kt b/app/src/main/java/org/fnives/tiktokdownloader/di/module/NetworkModule.kt index 5e05cce..507bfa1 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/di/module/NetworkModule.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/di/module/NetworkModule.kt @@ -7,6 +7,7 @@ import org.fnives.tiktokdownloader.data.network.TikTokDownloadRemoteSource import org.fnives.tiktokdownloader.data.network.TikTokRetrofitService import org.fnives.tiktokdownloader.data.network.parsing.TikTokWebPageConverterFactory import org.fnives.tiktokdownloader.data.network.parsing.converter.ThrowIfIsCaptchaResponse +import org.fnives.tiktokdownloader.data.network.parsing.converter.ThrowIfVideoIsDeletedResponse import org.fnives.tiktokdownloader.data.network.parsing.converter.VideoFileUrlConverter import org.fnives.tiktokdownloader.data.network.session.CookieSavingInterceptor import org.fnives.tiktokdownloader.data.network.session.CookieStore @@ -18,8 +19,11 @@ class NetworkModule(private val delayBeforeRequest: Long) { private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse get() = ThrowIfIsCaptchaResponse() + private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse + get() = ThrowIfVideoIsDeletedResponse() + private val tikTokConverterFactory: Converter.Factory - get() = TikTokWebPageConverterFactory(throwIfIsCaptchaResponse) + get() = TikTokWebPageConverterFactory(throwIfIsCaptchaResponse, throwIfVideoIsDeletedResponse) private val cookieSavingInterceptor: CookieSavingInterceptor by lazy { CookieSavingInterceptor() } @@ -48,5 +52,5 @@ class NetworkModule(private val delayBeforeRequest: Long) { get() = retrofit.create(TikTokRetrofitService::class.java) val tikTokDownloadRemoteSource: TikTokDownloadRemoteSource - get() = TikTokDownloadRemoteSource(delayBeforeRequest, tikTokRetrofitService, cookieStore, VideoFileUrlConverter(throwIfIsCaptchaResponse)) + get() = TikTokDownloadRemoteSource(delayBeforeRequest, tikTokRetrofitService, cookieStore, VideoFileUrlConverter(throwIfIsCaptchaResponse, throwIfVideoIsDeletedResponse)) } \ No newline at end of file diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainActivity.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainActivity.kt index 03dc697..55aa62e 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainActivity.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainActivity.kt @@ -104,6 +104,7 @@ class MainActivity : AppCompatActivity() { MainViewModel.ErrorMessage.STORAGE -> R.string.storage_error MainViewModel.ErrorMessage.CAPTCHA -> R.string.captcha_error MainViewModel.ErrorMessage.UNKNOWN -> R.string.unexpected_error + MainViewModel.ErrorMessage.DELETED -> R.string.video_deleted_error } private fun animateFabClicked(downloadFab: FloatingActionButton) { diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainViewModel.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainViewModel.kt index 25d21e1..c205f64 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainViewModel.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/main/MainViewModel.kt @@ -45,6 +45,7 @@ class MainViewModel( ProcessState.StorageError -> ErrorMessage.STORAGE ProcessState.CaptchaError -> ErrorMessage.CAPTCHA ProcessState.UnknownError -> ErrorMessage.UNKNOWN + ProcessState.VideoDeletedError -> ErrorMessage.DELETED } val refreshActionVisibility = when (it) { is ProcessState.Processing, @@ -54,6 +55,7 @@ class MainViewModel( ProcessState.ParsingError, ProcessState.StorageError, ProcessState.UnknownError, + ProcessState.VideoDeletedError, ProcessState.CaptchaError -> true } _errorMessage.postValue(errorMessage?.let(::Event)) @@ -71,7 +73,7 @@ class MainViewModel( } enum class ErrorMessage { - NETWORK, PARSING, STORAGE, CAPTCHA, UNKNOWN + NETWORK, PARSING, STORAGE, CAPTCHA, UNKNOWN, DELETED } enum class Screen { diff --git a/app/src/main/java/org/fnives/tiktokdownloader/ui/service/QueueServiceViewModel.kt b/app/src/main/java/org/fnives/tiktokdownloader/ui/service/QueueServiceViewModel.kt index 0f5e4f4..09f810b 100644 --- a/app/src/main/java/org/fnives/tiktokdownloader/ui/service/QueueServiceViewModel.kt +++ b/app/src/main/java/org/fnives/tiktokdownloader/ui/service/QueueServiceViewModel.kt @@ -52,6 +52,8 @@ class QueueServiceViewModel( ProcessState.Finished -> NotificationState.Finish ProcessState.CaptchaError -> NotificationState.Error(R.string.captcha_error) + + ProcessState.VideoDeletedError -> NotificationState.Error(R.string.video_deleted_error) } _notificationState.postValue(value) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 410cc0d..ed48072 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,7 @@ Parsing Error Failed to Store Video Unexpected Error + Video seems to be Deleted Permission Needed External Storage permission is needed in order to save the video to your device OK diff --git a/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCaseTest.kt b/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCaseTest.kt index 374d0b5..5e75bdd 100644 --- a/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCaseTest.kt +++ b/app/src/test/java/org/fnives/tiktokdownloader/data/usecase/VideoDownloadingProcessorUseCaseTest.kt @@ -117,7 +117,7 @@ class VideoDownloadingProcessorUseCaseTest { fun GIVEN_one_pending_video_AND_network_error_WHEN_observing_THEN_error_is_emited() = runBlocking { val videoInPending = VideoInPending("alma", "banan") videoInPendingMutableFlow.value = listOf(videoInPending) - whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } + whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException(html = "") } val expected = ProcessState.NetworkError val expectedList = listOf(ProcessState.Processing(videoInPending), expected) @@ -137,7 +137,7 @@ class VideoDownloadingProcessorUseCaseTest { fun GIVEN_one_pending_video_AND_parsing_error_WHEN_observing_THEN_parsingError_is_emited() = runBlocking { val videoInPending = VideoInPending("alma", "banan") videoInPendingMutableFlow.value = listOf(videoInPending) - whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw ParsingException() } + whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw ParsingException(html = "") } val expected = ProcessState.ParsingError val expectedList = listOf(ProcessState.Processing(videoInPending), expected) @@ -167,7 +167,7 @@ class VideoDownloadingProcessorUseCaseTest { videoInPendingMutableFlow.value = listOf(videoInPending) var specificException = true whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { - throw if (specificException) NetworkException().also { specificException = false } else Throwable() + throw if (specificException) NetworkException(html = "").also { specificException = false } else Throwable() } val inProgressItem = ProcessState.Processing(videoInPending) val expectedList = listOf(inProgressItem, ProcessState.NetworkError, inProgressItem, ProcessState.UnknownError) @@ -186,7 +186,7 @@ class VideoDownloadingProcessorUseCaseTest { videoInPendingMutableFlow.value = listOf(videoInPending) var specificException = true whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { - throw if (specificException) ParsingException().also { specificException = false } else Throwable() + throw if (specificException) ParsingException(html = "").also { specificException = false } else Throwable() } val inProgressItem = ProcessState.Processing(videoInPending) val expectedList = listOf(inProgressItem, ProcessState.ParsingError, inProgressItem, ProcessState.UnknownError) @@ -205,7 +205,7 @@ class VideoDownloadingProcessorUseCaseTest { videoInPendingMutableFlow.value = listOf(videoInPending) var specificException = true whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { - throw if (specificException) Throwable().also { specificException = false } else NetworkException() + throw if (specificException) Throwable().also { specificException = false } else NetworkException(html = "") } val inProgressItem = ProcessState.Processing(videoInPending) val expectedList = listOf(inProgressItem, ProcessState.UnknownError, inProgressItem, ProcessState.NetworkError) @@ -229,7 +229,7 @@ class VideoDownloadingProcessorUseCaseTest { sut.fetchVideoInState() specificException = false - NetworkException() + NetworkException(html = "") } else { Throwable() } @@ -250,7 +250,7 @@ class VideoDownloadingProcessorUseCaseTest { val videoInPending = VideoInPending("alma", "banan") videoInPendingMutableFlow.value = listOf(videoInPending) whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { - throw NetworkException() + throw NetworkException(html = "") } val inProgressItem = ProcessState.Processing(videoInPending) val expectedList = listOf(inProgressItem, ProcessState.NetworkError) @@ -433,7 +433,7 @@ class VideoDownloadingProcessorUseCaseTest { ProcessState.CaptchaError ) whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { - throw CaptchaRequiredException() + throw CaptchaRequiredException(html = "") } val resultList = async(testDispatcher) { sut.processState.take(2).toList() } @@ -459,7 +459,7 @@ class VideoDownloadingProcessorUseCaseTest { fun GIVEN_one_pending_video_AND_not_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking { val videoInPending = VideoInPending("alma", "banan") videoInPendingMutableFlow.value = listOf(videoInPending) - whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } + whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException(html = "") } val resultList = async(testDispatcher) { sut.processState.take(2).toList() } testDispatcher.advanceTimeBy(199) @@ -473,7 +473,7 @@ class VideoDownloadingProcessorUseCaseTest { fun GIVEN_one_pending_video_AND_but_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking { val videoInPending = VideoInPending("alma", "banan") videoInPendingMutableFlow.value = listOf(videoInPending) - whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } + whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException(html = "") } val resultList = async(testDispatcher) { sut.processState.take(2).toList() } testDispatcher.advanceTimeBy(201) diff --git a/app/src/test/java/org/fnives/uptodate/TikTokDownloadRemoteSourceUpToDateTest.kt b/app/src/test/java/org/fnives/uptodate/TikTokDownloadRemoteSourceUpToDateTest.kt index e0b199e..50905e2 100644 --- a/app/src/test/java/org/fnives/uptodate/TikTokDownloadRemoteSourceUpToDateTest.kt +++ b/app/src/test/java/org/fnives/uptodate/TikTokDownloadRemoteSourceUpToDateTest.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.runBlocking import org.apache.commons.io.FileUtils import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.network.TikTokDownloadRemoteSource +import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException import org.fnives.tiktokdownloader.di.module.NetworkModule import org.fnives.tiktokdownloader.helper.getResourceFile import org.junit.jupiter.api.Assertions @@ -37,7 +38,7 @@ class TikTokDownloadRemoteSourceUpToDateTest { actualFile.delete() actualFile.createNewFile() actualFile.deleteOnExit() - val expectedFileOptions = EXPECTED_FILE_PATHS.map{getResourceFile(it)} + val expectedFileOptions = EXPECTED_FILE_PATHS.map { getResourceFile(it) } actualFile.writeText("") runBlocking { sut.getVideo(parameter).byteStream }.use { inputStream -> @@ -45,15 +46,30 @@ class TikTokDownloadRemoteSourceUpToDateTest { inputStream.copyTo(outputStream) } } - val doesAnyIsTheSameFile = expectedFileOptions.any { expectedFile-> + val doesAnyIsTheSameFile = expectedFileOptions.any { expectedFile -> FileUtils.contentEquals(expectedFile, actualFile) } - Assertions.assertTrue(doesAnyIsTheSameFile, "The Downloaded file Is Not Matching the expected") + Assertions.assertTrue( + doesAnyIsTheSameFile, + "The Downloaded file Is Not Matching the expected" + ) + } + + @Timeout(value = 120) + @Test + fun GIVEN_deleted_WHEN_downloading_THEN_proper_exception_is_thrown() { + val parameter = VideoInPending("123", DELETED_VIDEO_URL) + Assertions.assertThrows(VideoDeletedException::class.java) { + runBlocking { sut.getVideo(parameter) } + } } companion object { private const val ACTUAL_FILE_PATH = "actual.mp4" - private val EXPECTED_FILE_PATHS = listOf("video/expected_option_1.mp4","video/expected_option_2.mp4") + private val EXPECTED_FILE_PATHS = + listOf("video/expected_option_1.mp4", "video/expected_option_2.mp4") private const val SUBJECT_VIDEO_URL = "https://vm.tiktok.com/ZSQG7SMf/" + private const val PRIVATE_VIDEO_URL = "https://vm.tiktok.com/ZNdM4EjTQ/" + private const val DELETED_VIDEO_URL = "https://vm.tiktok.com/ZNdMVM4WG/" } } \ No newline at end of file diff --git a/app/src/test/resources/response/deleted_video.html b/app/src/test/resources/response/deleted_video.html new file mode 100644 index 0000000..eea581c --- /dev/null +++ b/app/src/test/resources/response/deleted_video.html @@ -0,0 +1,5779 @@ + + + + + + TikTok - Make Your Day + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file