#17 Show specific error if video is deleted

This commit is contained in:
Gergely Hegedus 2025-05-13 18:01:27 +03:00
parent e8afb551ee
commit b1e44dce43
22 changed files with 5945 additions and 65 deletions

View file

@ -4,10 +4,11 @@ sealed class ProcessState {
data class Processing(val videoInPending: VideoInPending) : ProcessState() data class Processing(val videoInPending: VideoInPending) : ProcessState()
data class Processed(val videoDownloaded: VideoDownloaded) : ProcessState() data class Processed(val videoDownloaded: VideoDownloaded) : ProcessState()
object NetworkError : ProcessState() data object NetworkError : ProcessState()
object ParsingError : ProcessState() data object ParsingError : ProcessState()
object CaptchaError : ProcessState() data object VideoDeletedError : ProcessState()
object UnknownError : ProcessState() data object CaptchaError : ProcessState()
object StorageError : ProcessState() data object UnknownError : ProcessState()
object Finished: ProcessState() data object StorageError : ProcessState()
data object Finished: ProcessState()
} }

View file

@ -7,8 +7,10 @@ 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
import org.fnives.tiktokdownloader.data.network.exceptions.HtmlException
import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException
import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException 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.converter.VideoFileUrlConverter
import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl
import org.fnives.tiktokdownloader.data.network.session.CookieStore import org.fnives.tiktokdownloader.data.network.session.CookieStore
@ -21,7 +23,8 @@ class TikTokDownloadRemoteSource(
) { ) {
@Throws(ParsingException::class, NetworkException::class, CaptchaRequiredException::class) @Throws(ParsingException::class, NetworkException::class, CaptchaRequiredException::class)
suspend fun getVideo(videoInPending: VideoInPending): VideoInSavingIntoFile = withContext(Dispatchers.IO) { suspend fun getVideo(videoInPending: VideoInPending): VideoInSavingIntoFile =
withContext(Dispatchers.IO) {
cookieStore.clear() cookieStore.clear()
wrapIntoProperException { wrapIntoProperException {
delay(delayBeforeRequest) // added just so captcha trigger may not happen delay(delayBeforeRequest) // added just so captcha trigger may not happen
@ -44,13 +47,18 @@ class TikTokDownloadRemoteSource(
VideoInSavingIntoFile( VideoInSavingIntoFile(
id = videoInPending.id, id = videoInPending.id,
url = videoInPending.url, url = videoInPending.url,
contentType = response.mediaType?.let { VideoInSavingIntoFile.ContentType(it.type, it.subtype) }, contentType = response.mediaType?.let {
VideoInSavingIntoFile.ContentType(
it.type,
it.subtype
)
},
byteStream = response.videoInputStream byteStream = response.videoInputStream
) )
} }
} }
@Throws(ParsingException::class, NetworkException::class) @Throws(ParsingException::class, NetworkException::class, VideoDeletedException::class)
private suspend fun <T> wrapIntoProperException(request: suspend () -> T): T = private suspend fun <T> wrapIntoProperException(request: suspend () -> T): T =
try { try {
request() request()
@ -58,7 +66,12 @@ class TikTokDownloadRemoteSource(
throw parsingException throw parsingException
} catch (captchaRequiredException: CaptchaRequiredException) { } catch (captchaRequiredException: CaptchaRequiredException) {
throw captchaRequiredException throw captchaRequiredException
} catch (videoDeletedException: VideoDeletedException) {
throw videoDeletedException
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
throw NetworkException(cause = throwable) throw NetworkException(
cause = throwable,
html = (throwable as? HtmlException)?.html.orEmpty()
)
} }
} }

View file

@ -1,3 +1,6 @@
package org.fnives.tiktokdownloader.data.network.exceptions package org.fnives.tiktokdownloader.data.network.exceptions
class CaptchaRequiredException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) class CaptchaRequiredException(
message: String? = null, cause: Throwable? = null,
override val html: String,
) : Throwable(message, cause), HtmlException

View file

@ -0,0 +1,5 @@
package org.fnives.tiktokdownloader.data.network.exceptions
interface HtmlException {
val html: String
}

View file

@ -1,3 +1,6 @@
package org.fnives.tiktokdownloader.data.network.exceptions package org.fnives.tiktokdownloader.data.network.exceptions
class NetworkException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) class NetworkException(
message: String? = null, cause: Throwable? = null,
override val html: String,
) : Throwable(message, cause), HtmlException

View file

@ -2,4 +2,7 @@ package org.fnives.tiktokdownloader.data.network.exceptions
import java.io.IOException import java.io.IOException
class ParsingException(message: String? = null, cause: Throwable? = null) : IOException(message, cause) class ParsingException(
message: String? = null, cause: Throwable? = null,
override val html: String,
) : IOException(message, cause), HtmlException

View file

@ -0,0 +1,4 @@
package org.fnives.tiktokdownloader.data.network.exceptions
class VideoDeletedException(override val html: String) : Throwable(),
HtmlException

View file

@ -3,6 +3,7 @@ package org.fnives.tiktokdownloader.data.network.parsing
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.fnives.tiktokdownloader.data.network.parsing.converter.ActualVideoPageUrlConverter 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.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.VideoFileUrlConverter
import org.fnives.tiktokdownloader.data.network.parsing.converter.VideoResponseConverter import org.fnives.tiktokdownloader.data.network.parsing.converter.VideoResponseConverter
import org.fnives.tiktokdownloader.data.network.parsing.response.ActualVideoPageUrl import org.fnives.tiktokdownloader.data.network.parsing.response.ActualVideoPageUrl
@ -12,7 +13,10 @@ import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import java.lang.reflect.Type 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( override fun responseBodyConverter(
type: Type, type: Type,
@ -20,8 +24,12 @@ class TikTokWebPageConverterFactory(private val throwIfIsCaptchaResponse: ThrowI
retrofit: Retrofit retrofit: Retrofit
): Converter<ResponseBody, *>? = ): Converter<ResponseBody, *>? =
when (type) { when (type) {
ActualVideoPageUrl::class.java -> ActualVideoPageUrlConverter(throwIfIsCaptchaResponse) ActualVideoPageUrl::class.java -> ActualVideoPageUrlConverter(
VideoFileUrl::class.java -> VideoFileUrlConverter(throwIfIsCaptchaResponse) throwIfIsCaptchaResponse,
throwIfVideoIsDeletedResponse
)
VideoFileUrl::class.java -> VideoFileUrlConverter(throwIfIsCaptchaResponse, throwIfVideoIsDeletedResponse)
VideoResponse::class.java -> VideoResponseConverter() VideoResponse::class.java -> VideoResponseConverter()
else -> super.responseBodyConverter(type, annotations, retrofit) else -> super.responseBodyConverter(type, annotations, retrofit)
} }

View file

@ -6,7 +6,8 @@ import org.fnives.tiktokdownloader.data.network.parsing.response.ActualVideoPage
import kotlin.jvm.Throws import kotlin.jvm.Throws
class ActualVideoPageUrlConverter( class ActualVideoPageUrlConverter(
private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse,
private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse
) : ParsingExceptionThrowingConverter<ActualVideoPageUrl>() { ) : ParsingExceptionThrowingConverter<ActualVideoPageUrl>() {
@Throws(IndexOutOfBoundsException::class, CaptchaRequiredException::class) @Throws(IndexOutOfBoundsException::class, CaptchaRequiredException::class)
@ -15,6 +16,7 @@ class ActualVideoPageUrlConverter(
return try { return try {
val actualVideoPageUrl = responseBodyAsString val actualVideoPageUrl = responseBodyAsString
.also(throwIfIsCaptchaResponse::invoke) .also(throwIfIsCaptchaResponse::invoke)
.also(throwIfVideoIsDeletedResponse::invoke)
.split("rel=\"canonical\" href=\"")[1] .split("rel=\"canonical\" href=\"")[1]
.split("\"")[0] .split("\"")[0]

View file

@ -2,25 +2,32 @@ package org.fnives.tiktokdownloader.data.network.parsing.converter
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException 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.ParsingException
import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException
import retrofit2.Converter import retrofit2.Converter
abstract class ParsingExceptionThrowingConverter<T> : Converter<ResponseBody, T> { abstract class ParsingExceptionThrowingConverter<T> : Converter<ResponseBody, T> {
@Throws(ParsingException::class, CaptchaRequiredException::class) @Throws(ParsingException::class, CaptchaRequiredException::class, VideoDeletedException::class)
final override fun convert(value: ResponseBody): T? = final override fun convert(value: ResponseBody): T? =
doActionSafely { doActionSafely {
convertSafely(value) convertSafely(value)
} }
@Throws(ParsingException::class, CaptchaRequiredException::class) @Throws(ParsingException::class, CaptchaRequiredException::class, VideoDeletedException::class)
fun doActionSafely(action: () -> T): T { fun doActionSafely(action: () -> T): T {
try { try {
return action() return action()
} catch (captchaRequiredException: CaptchaRequiredException) { } catch (captchaRequiredException: CaptchaRequiredException) {
throw captchaRequiredException throw captchaRequiredException
} catch(videoDeletedException: VideoDeletedException) {
throw videoDeletedException
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
throw ParsingException(cause = throwable) throw ParsingException(
cause = throwable,
html = (throwable as? HtmlException)?.html.orEmpty()
)
} }
} }

View file

@ -1,16 +1,15 @@
package org.fnives.tiktokdownloader.data.network.parsing.converter package org.fnives.tiktokdownloader.data.network.parsing.converter
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
import kotlin.jvm.Throws
class ThrowIfIsCaptchaResponse { class ThrowIfIsCaptchaResponse {
@Throws(CaptchaRequiredException::class) @Throws(CaptchaRequiredException::class)
fun invoke(html: String) { fun invoke(html: String) {
if (html.isEmpty()) { if (html.isEmpty()) {
throw CaptchaRequiredException("Empty body") throw CaptchaRequiredException("Empty body", html = html)
} else if (html.contains("captcha.js")) { } else if (html.contains("captcha.js")) {
throw CaptchaRequiredException("Contains Captcha keyword") throw CaptchaRequiredException("Contains Captcha keyword", html = html)
} }
} }
} }

View file

@ -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)
}
}
}

View file

@ -7,7 +7,8 @@ import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException
import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl
class VideoFileUrlConverter( class VideoFileUrlConverter(
private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse,
private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse,
) : ParsingExceptionThrowingConverter<VideoFileUrl>() { ) : ParsingExceptionThrowingConverter<VideoFileUrl>() {
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class) @Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class)
@ -23,6 +24,7 @@ class VideoFileUrlConverter(
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class) @Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class)
private fun convert(responseBody: String): VideoFileUrl { private fun convert(responseBody: String): VideoFileUrl {
val html = responseBody.also(throwIfIsCaptchaResponse::invoke) val html = responseBody.also(throwIfIsCaptchaResponse::invoke)
.also(throwIfVideoIsDeletedResponse::invoke)
val url = tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") } val url = tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") }
?: tryToParseVideoSrc(html).also { Logger.logMessage("parsed video src = $it") } ?: 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")

View file

@ -2,6 +2,7 @@ package org.fnives.tiktokdownloader.data.usecase
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow 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.CaptchaRequiredException
import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException
import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException
import org.fnives.tiktokdownloader.data.network.exceptions.VideoDeletedException
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class VideoDownloadingProcessorUseCase( class VideoDownloadingProcessorUseCase(
@ -42,6 +44,8 @@ class VideoDownloadingProcessorUseCase(
) { ) {
private val fetch = MutableStateFlow(ProcessingState.RUNNING) private val fetch = MutableStateFlow(ProcessingState.RUNNING)
@OptIn(ExperimentalCoroutinesApi::class)
private val _processState by lazy { private val _processState by lazy {
combineIntoPair(fetch, videoInPendingLocalSource.observeFirstPendingVideo()) combineIntoPair(fetch, videoInPendingLocalSource.observeFirstPendingVideo())
.filter { it.first == ProcessingState.RUNNING } .filter { it.first == ProcessingState.RUNNING }
@ -84,13 +88,17 @@ class VideoDownloadingProcessorUseCase(
videoInPendingLocalSource.removeVideoFromQueue(videoInPending) videoInPendingLocalSource.removeVideoFromQueue(videoInPending)
alreadyDownloaded alreadyDownloaded
} }
captchaTimeoutLocalSource.isInCaptchaTimeout() -> { captchaTimeoutLocalSource.isInCaptchaTimeout() -> {
throw CaptchaRequiredException("In Captcha Timeout!") throw CaptchaRequiredException("In Captcha Timeout!", html = "")
} }
else -> { else -> {
videoInProgressLocalSource.markVideoAsInProgress(videoInPending) videoInProgressLocalSource.markVideoAsInProgress(videoInPending)
val videoInSavingIntoFile: VideoInSavingIntoFile = tikTokDownloadRemoteSource.getVideo(videoInPending) val videoInSavingIntoFile: VideoInSavingIntoFile =
val videoDownloaded: VideoDownloaded = videoDownloadedLocalSource.saveVideo(videoInSavingIntoFile) tikTokDownloadRemoteSource.getVideo(videoInPending)
val videoDownloaded: VideoDownloaded =
videoDownloadedLocalSource.saveVideo(videoInSavingIntoFile)
videoInPendingLocalSource.removeVideoFromQueue(videoInPending) videoInPendingLocalSource.removeVideoFromQueue(videoInPending)
videoDownloaded videoDownloaded
@ -102,6 +110,8 @@ class VideoDownloadingProcessorUseCase(
ProcessState.NetworkError ProcessState.NetworkError
} catch (parsingException: ParsingException) { } catch (parsingException: ParsingException) {
ProcessState.ParsingError ProcessState.ParsingError
} catch (videoDeletedException: VideoDeletedException) {
ProcessState.VideoDeletedError
} catch (storageException: StorageException) { } catch (storageException: StorageException) {
ProcessState.StorageError ProcessState.StorageError
} catch (captchaRequiredException: CaptchaRequiredException) { } catch (captchaRequiredException: CaptchaRequiredException) {
@ -124,10 +134,12 @@ class VideoDownloadingProcessorUseCase(
is ProcessState.Processing, is ProcessState.Processing,
is ProcessState.Processed, is ProcessState.Processed,
ProcessState.Finished -> false ProcessState.Finished -> false
ProcessState.NetworkError, ProcessState.NetworkError,
ProcessState.ParsingError, ProcessState.ParsingError,
ProcessState.StorageError, ProcessState.StorageError,
ProcessState.UnknownError, ProcessState.UnknownError,
ProcessState.VideoDeletedError,
ProcessState.CaptchaError -> true ProcessState.CaptchaError -> true
} }

View file

@ -7,6 +7,7 @@ import org.fnives.tiktokdownloader.data.network.TikTokDownloadRemoteSource
import org.fnives.tiktokdownloader.data.network.TikTokRetrofitService import org.fnives.tiktokdownloader.data.network.TikTokRetrofitService
import org.fnives.tiktokdownloader.data.network.parsing.TikTokWebPageConverterFactory 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.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.VideoFileUrlConverter
import org.fnives.tiktokdownloader.data.network.session.CookieSavingInterceptor import org.fnives.tiktokdownloader.data.network.session.CookieSavingInterceptor
import org.fnives.tiktokdownloader.data.network.session.CookieStore import org.fnives.tiktokdownloader.data.network.session.CookieStore
@ -18,8 +19,11 @@ class NetworkModule(private val delayBeforeRequest: Long) {
private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse
get() = ThrowIfIsCaptchaResponse() get() = ThrowIfIsCaptchaResponse()
private val throwIfVideoIsDeletedResponse: ThrowIfVideoIsDeletedResponse
get() = ThrowIfVideoIsDeletedResponse()
private val tikTokConverterFactory: Converter.Factory private val tikTokConverterFactory: Converter.Factory
get() = TikTokWebPageConverterFactory(throwIfIsCaptchaResponse) get() = TikTokWebPageConverterFactory(throwIfIsCaptchaResponse, throwIfVideoIsDeletedResponse)
private val cookieSavingInterceptor: CookieSavingInterceptor by lazy { CookieSavingInterceptor() } private val cookieSavingInterceptor: CookieSavingInterceptor by lazy { CookieSavingInterceptor() }
@ -48,5 +52,5 @@ class NetworkModule(private val delayBeforeRequest: Long) {
get() = retrofit.create(TikTokRetrofitService::class.java) get() = retrofit.create(TikTokRetrofitService::class.java)
val tikTokDownloadRemoteSource: TikTokDownloadRemoteSource val tikTokDownloadRemoteSource: TikTokDownloadRemoteSource
get() = TikTokDownloadRemoteSource(delayBeforeRequest, tikTokRetrofitService, cookieStore, VideoFileUrlConverter(throwIfIsCaptchaResponse)) get() = TikTokDownloadRemoteSource(delayBeforeRequest, tikTokRetrofitService, cookieStore, VideoFileUrlConverter(throwIfIsCaptchaResponse, throwIfVideoIsDeletedResponse))
} }

View file

@ -104,6 +104,7 @@ class MainActivity : AppCompatActivity() {
MainViewModel.ErrorMessage.STORAGE -> R.string.storage_error MainViewModel.ErrorMessage.STORAGE -> R.string.storage_error
MainViewModel.ErrorMessage.CAPTCHA -> R.string.captcha_error MainViewModel.ErrorMessage.CAPTCHA -> R.string.captcha_error
MainViewModel.ErrorMessage.UNKNOWN -> R.string.unexpected_error MainViewModel.ErrorMessage.UNKNOWN -> R.string.unexpected_error
MainViewModel.ErrorMessage.DELETED -> R.string.video_deleted_error
} }
private fun animateFabClicked(downloadFab: FloatingActionButton) { private fun animateFabClicked(downloadFab: FloatingActionButton) {

View file

@ -45,6 +45,7 @@ class MainViewModel(
ProcessState.StorageError -> ErrorMessage.STORAGE ProcessState.StorageError -> ErrorMessage.STORAGE
ProcessState.CaptchaError -> ErrorMessage.CAPTCHA ProcessState.CaptchaError -> ErrorMessage.CAPTCHA
ProcessState.UnknownError -> ErrorMessage.UNKNOWN ProcessState.UnknownError -> ErrorMessage.UNKNOWN
ProcessState.VideoDeletedError -> ErrorMessage.DELETED
} }
val refreshActionVisibility = when (it) { val refreshActionVisibility = when (it) {
is ProcessState.Processing, is ProcessState.Processing,
@ -54,6 +55,7 @@ class MainViewModel(
ProcessState.ParsingError, ProcessState.ParsingError,
ProcessState.StorageError, ProcessState.StorageError,
ProcessState.UnknownError, ProcessState.UnknownError,
ProcessState.VideoDeletedError,
ProcessState.CaptchaError -> true ProcessState.CaptchaError -> true
} }
_errorMessage.postValue(errorMessage?.let(::Event)) _errorMessage.postValue(errorMessage?.let(::Event))
@ -71,7 +73,7 @@ class MainViewModel(
} }
enum class ErrorMessage { enum class ErrorMessage {
NETWORK, PARSING, STORAGE, CAPTCHA, UNKNOWN NETWORK, PARSING, STORAGE, CAPTCHA, UNKNOWN, DELETED
} }
enum class Screen { enum class Screen {

View file

@ -52,6 +52,8 @@ class QueueServiceViewModel(
ProcessState.Finished -> NotificationState.Finish ProcessState.Finished -> NotificationState.Finish
ProcessState.CaptchaError -> ProcessState.CaptchaError ->
NotificationState.Error(R.string.captcha_error) NotificationState.Error(R.string.captcha_error)
ProcessState.VideoDeletedError -> NotificationState.Error(R.string.video_deleted_error)
} }
_notificationState.postValue(value) _notificationState.postValue(value)
} }

View file

@ -19,6 +19,7 @@
<string name="parsing_error">Parsing Error</string> <string name="parsing_error">Parsing Error</string>
<string name="storage_error">Failed to Store Video</string> <string name="storage_error">Failed to Store Video</string>
<string name="unexpected_error">Unexpected Error</string> <string name="unexpected_error">Unexpected Error</string>
<string name="video_deleted_error">Video seems to be Deleted</string>
<string name="permission_request">Permission Needed</string> <string name="permission_request">Permission Needed</string>
<string name="permission_rationale">External Storage permission is needed in order to save the video to your device</string> <string name="permission_rationale">External Storage permission is needed in order to save the video to your device</string>
<string name="ok">OK</string> <string name="ok">OK</string>

View file

@ -117,7 +117,7 @@ class VideoDownloadingProcessorUseCaseTest {
fun GIVEN_one_pending_video_AND_network_error_WHEN_observing_THEN_error_is_emited() = runBlocking { fun GIVEN_one_pending_video_AND_network_error_WHEN_observing_THEN_error_is_emited() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException(html = "") }
val expected = ProcessState.NetworkError val expected = ProcessState.NetworkError
val expectedList = listOf(ProcessState.Processing(videoInPending), expected) 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 { fun GIVEN_one_pending_video_AND_parsing_error_WHEN_observing_THEN_parsingError_is_emited() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw ParsingException() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw ParsingException(html = "") }
val expected = ProcessState.ParsingError val expected = ProcessState.ParsingError
val expectedList = listOf(ProcessState.Processing(videoInPending), expected) val expectedList = listOf(ProcessState.Processing(videoInPending), expected)
@ -167,7 +167,7 @@ class VideoDownloadingProcessorUseCaseTest {
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { 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 inProgressItem = ProcessState.Processing(videoInPending)
val expectedList = listOf(inProgressItem, ProcessState.NetworkError, inProgressItem, ProcessState.UnknownError) val expectedList = listOf(inProgressItem, ProcessState.NetworkError, inProgressItem, ProcessState.UnknownError)
@ -186,7 +186,7 @@ class VideoDownloadingProcessorUseCaseTest {
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { 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 inProgressItem = ProcessState.Processing(videoInPending)
val expectedList = listOf(inProgressItem, ProcessState.ParsingError, inProgressItem, ProcessState.UnknownError) val expectedList = listOf(inProgressItem, ProcessState.ParsingError, inProgressItem, ProcessState.UnknownError)
@ -205,7 +205,7 @@ class VideoDownloadingProcessorUseCaseTest {
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { 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 inProgressItem = ProcessState.Processing(videoInPending)
val expectedList = listOf(inProgressItem, ProcessState.UnknownError, inProgressItem, ProcessState.NetworkError) val expectedList = listOf(inProgressItem, ProcessState.UnknownError, inProgressItem, ProcessState.NetworkError)
@ -229,7 +229,7 @@ class VideoDownloadingProcessorUseCaseTest {
sut.fetchVideoInState() sut.fetchVideoInState()
specificException = false specificException = false
NetworkException() NetworkException(html = "")
} else { } else {
Throwable() Throwable()
} }
@ -250,7 +250,7 @@ class VideoDownloadingProcessorUseCaseTest {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then {
throw NetworkException() throw NetworkException(html = "")
} }
val inProgressItem = ProcessState.Processing(videoInPending) val inProgressItem = ProcessState.Processing(videoInPending)
val expectedList = listOf(inProgressItem, ProcessState.NetworkError) val expectedList = listOf(inProgressItem, ProcessState.NetworkError)
@ -433,7 +433,7 @@ class VideoDownloadingProcessorUseCaseTest {
ProcessState.CaptchaError ProcessState.CaptchaError
) )
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then {
throw CaptchaRequiredException() throw CaptchaRequiredException(html = "")
} }
val resultList = async(testDispatcher) { sut.processState.take(2).toList() } 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<Unit> { fun GIVEN_one_pending_video_AND_not_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) 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() } val resultList = async(testDispatcher) { sut.processState.take(2).toList() }
testDispatcher.advanceTimeBy(199) 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<Unit> { fun GIVEN_one_pending_video_AND_but_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) 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() } val resultList = async(testDispatcher) { sut.processState.take(2).toList() }
testDispatcher.advanceTimeBy(201) testDispatcher.advanceTimeBy(201)

View file

@ -4,6 +4,7 @@ import kotlinx.coroutines.runBlocking
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.fnives.tiktokdownloader.data.network.TikTokDownloadRemoteSource 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.di.module.NetworkModule
import org.fnives.tiktokdownloader.helper.getResourceFile import org.fnives.tiktokdownloader.helper.getResourceFile
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
@ -48,12 +49,27 @@ class TikTokDownloadRemoteSourceUpToDateTest {
val doesAnyIsTheSameFile = expectedFileOptions.any { expectedFile -> val doesAnyIsTheSameFile = expectedFileOptions.any { expectedFile ->
FileUtils.contentEquals(expectedFile, actualFile) 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 { companion object {
private const val ACTUAL_FILE_PATH = "actual.mp4" 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 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/"
} }
} }

File diff suppressed because it is too large Load diff