@ -28,6 +28,10 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Request
@ -71,6 +75,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import java.io.File
import java.util.UUID
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
import javax.inject.Inject
@HiltWorker
@HiltWorker
@ -81,7 +86,6 @@ class DownloadWorker @AssistedInject constructor(
private val cache : PagesCache ,
private val cache : PagesCache ,
private val localMangaRepository : LocalMangaRepository ,
private val localMangaRepository : LocalMangaRepository ,
private val mangaDataRepository : MangaDataRepository ,
private val mangaDataRepository : MangaDataRepository ,
private val settings : AppSettings ,
private val mangaRepositoryFactory : MangaRepository . Factory ,
private val mangaRepositoryFactory : MangaRepository . Factory ,
@LocalStorageChanges private val localStorageChanges : MutableSharedFlow < LocalManga ? > ,
@LocalStorageChanges private val localStorageChanges : MutableSharedFlow < LocalManga ? > ,
notificationFactoryFactory : DownloadNotificationFactory . Factory ,
notificationFactoryFactory : DownloadNotificationFactory . Factory ,
@ -89,16 +93,15 @@ class DownloadWorker @AssistedInject constructor(
private val notificationFactory = notificationFactoryFactory . create ( params . id )
private val notificationFactory = notificationFactoryFactory . create ( params . id )
private val notificationManager = appContext . getSystemService ( Context . NOTIFICATION _SERVICE ) as NotificationManager
private val notificationManager = appContext . getSystemService ( Context . NOTIFICATION _SERVICE ) as NotificationManager
private val slowdownDispatcher = DownloadSlowdownDispatcher ( mangaRepositoryFactory , SLOWDOWN _DELAY )
@Volatile
@Volatile
private var lastPublishedState : DownloadState ? = null
private var lastPublishedState : DownloadState ? = null
private val currentState : DownloadState
private val currentState : DownloadState
get ( ) = checkNotNull ( lastPublishedState )
get ( ) = checkNotNull ( lastPublishedState )
private val pausingHandle = PausingHandle ( )
private val timeLeftEstimator = TimeLeftEstimator ( )
private val timeLeftEstimator = TimeLeftEstimator ( )
private val notificationThrottler = Throttler ( 400 )
private val notificationThrottler = Throttler ( 400 )
private val pausingReceiver = PausingReceiver ( params . id , pausingHandle )
override suspend fun doWork ( ) : Result {
override suspend fun doWork ( ) : Result {
setForeground ( getForegroundInfo ( ) )
setForeground ( getForegroundInfo ( ) )
@ -109,7 +112,9 @@ class DownloadWorker @AssistedInject constructor(
val chaptersIds = inputData . getLongArray ( CHAPTERS _IDS ) ?. takeUnless { it . isEmpty ( ) }
val chaptersIds = inputData . getLongArray ( CHAPTERS _IDS ) ?. takeUnless { it . isEmpty ( ) }
val downloadedIds = getDoneChapters ( manga )
val downloadedIds = getDoneChapters ( manga )
return try {
return try {
downloadMangaImpl ( manga , chaptersIds , downloadedIds )
withContext ( PausingHandle ( ) ) {
downloadMangaImpl ( manga , chaptersIds , downloadedIds )
}
Result . success ( currentState . toWorkData ( ) )
Result . success ( currentState . toWorkData ( ) )
} catch ( e : CancellationException ) {
} catch ( e : CancellationException ) {
withContext ( NonCancellable ) {
withContext ( NonCancellable ) {
@ -153,6 +158,7 @@ class DownloadWorker @AssistedInject constructor(
) {
) {
var manga = subject
var manga = subject
val chaptersToSkip = excludedIds . toMutableSet ( )
val chaptersToSkip = excludedIds . toMutableSet ( )
val pausingReceiver = PausingReceiver ( id , PausingHandle . current ( ) )
withMangaLock ( manga ) {
withMangaLock ( manga ) {
ContextCompat . registerReceiver (
ContextCompat . registerReceiver (
applicationContext ,
applicationContext ,
@ -180,39 +186,47 @@ class DownloadWorker @AssistedInject constructor(
}
}
val chapters = getChapters ( mangaDetails , includedIds )
val chapters = getChapters ( mangaDetails , includedIds )
for ( ( chapterIndex , chapter ) in chapters . withIndex ( ) ) {
for ( ( chapterIndex , chapter ) in chapters . withIndex ( ) ) {
checkIsPaused ( )
if ( chaptersToSkip . remove ( chapter . id ) ) {
if ( chaptersToSkip . remove ( chapter . id ) ) {
publishState ( currentState . copy ( downloadedChapters = currentState . downloadedChapters + 1 ) )
publishState ( currentState . copy ( downloadedChapters = currentState . downloadedChapters + 1 ) )
continue
continue
}
}
val pages = runFailsafe ( pausingHandle ) {
val pages = runFailsafe {
repo . getPages ( chapter )
repo . getPages ( chapter )
}
}
for ( ( pageIndex , page ) in pages . withIndex ( ) ) {
val pageCounter = AtomicInteger ( 0 )
runFailsafe ( pausingHandle ) {
channelFlow {
val url = repo . getPageUrl ( page )
val semaphore = Semaphore ( MAX _PAGES _PARALLELISM )
val file = cache . get ( url )
for ( ( pageIndex , page ) in pages . withIndex ( ) ) {
?: downloadFile ( url , destination , tempFileName , repo . source )
checkIsPaused ( )
output . addPage (
launch {
chapter = chapter ,
semaphore . withPermit {
file = file ,
runFailsafe {
pageNumber = pageIndex ,
val url = repo . getPageUrl ( page )
ext = MimeTypeMap . getFileExtensionFromUrl ( url ) ,
val file = cache . get ( url )
)
?: downloadFile ( url , destination , tempFileName , repo . source )
output . addPage (
chapter = chapter ,
file = file ,
pageNumber = pageIndex ,
ext = MimeTypeMap . getFileExtensionFromUrl ( url ) ,
)
}
send ( pageIndex )
}
}
}
}
} . collect {
publishState (
publishState (
currentState . copy (
currentState . copy (
totalChapters = chapters . size ,
totalChapters = chapters . size ,
currentChapter = chapterIndex ,
currentChapter = chapterIndex ,
totalPages = pages . size ,
totalPages = pages . size ,
currentPage = pageIndex ,
currentPage = page Counter. incrementAndGet ( ) ,
isIndeterminate = false ,
isIndeterminate = false ,
eta = timeLeftEstimator . getEta ( ) ,
eta = timeLeftEstimator . getEta ( ) ,
) ,
) ,
)
)
if ( settings . isDownloadsSlowdownEnabled ) {
delay ( SLOWDOWN _DELAY )
}
}
}
if ( output . flushChapter ( chapter ) ) {
if ( output . flushChapter ( chapter ) ) {
runCatchingCancellable {
runCatchingCancellable {
@ -244,14 +258,9 @@ class DownloadWorker @AssistedInject constructor(
}
}
private suspend fun < R > runFailsafe (
private suspend fun < R > runFailsafe (
pausingHandle : PausingHandle ,
block : suspend ( ) -> R ,
block : suspend ( ) -> R ,
) : R {
) : R {
if ( pausingHandle . isPaused ) {
checkIsPaused ( )
publishState ( currentState . copy ( isPaused = true , eta = - 1L ) )
pausingHandle . awaitResumed ( )
publishState ( currentState . copy ( isPaused = false ) )
}
var countDown = MAX _FAILSAFE _ATTEMPTS
var countDown = MAX _FAILSAFE _ATTEMPTS
failsafe @ while ( true ) {
failsafe @ while ( true ) {
try {
try {
@ -266,9 +275,13 @@ class DownloadWorker @AssistedInject constructor(
) ,
) ,
)
)
countDown = MAX _FAILSAFE _ATTEMPTS
countDown = MAX _FAILSAFE _ATTEMPTS
val pausingHandle = PausingHandle . current ( )
pausingHandle . pause ( )
pausingHandle . pause ( )
pausingHandle . awaitResumed ( )
try {
publishState ( currentState . copy ( isPaused = false , error = null ) )
pausingHandle . awaitResumed ( )
} finally {
publishState ( currentState . copy ( isPaused = false , error = null ) )
}
} else {
} else {
countDown --
countDown --
val retryDelay = if ( e is TooManyRequestExceptions ) {
val retryDelay = if ( e is TooManyRequestExceptions ) {
@ -282,6 +295,18 @@ class DownloadWorker @AssistedInject constructor(
}
}
}
}
private suspend fun checkIsPaused ( ) {
val pausingHandle = PausingHandle . current ( )
if ( pausingHandle . isPaused ) {
publishState ( currentState . copy ( isPaused = true , eta = - 1L ) )
try {
pausingHandle . awaitResumed ( )
} finally {
publishState ( currentState . copy ( isPaused = false ) )
}
}
}
private suspend fun downloadFile (
private suspend fun downloadFile (
url : String ,
url : String ,
destination : File ,
destination : File ,
@ -295,13 +320,19 @@ class DownloadWorker @AssistedInject constructor(
. cacheControl ( CommonHeaders . CACHE _CONTROL _NO _STORE )
. cacheControl ( CommonHeaders . CACHE _CONTROL _NO _STORE )
. get ( )
. get ( )
. build ( )
. build ( )
slowdownDispatcher . delay ( source )
val call = okHttp . newCall ( request )
val call = okHttp . newCall ( request )
val file = File ( destination , tempFileName )
val file = File ( destination , tempFileName )
val response = call . clone ( ) . await ( )
try {
checkNotNull ( response . body ) . use { body ->
val response = call . clone ( ) . await ( )
file . sink ( append = false ) . buffer ( ) . use {
checkNotNull ( response . body ) . use { body ->
it . writeAllCancellable ( body . source ( ) )
file . sink ( append = false ) . buffer ( ) . use {
it . writeAllCancellable ( body . source ( ) )
}
}
}
} catch ( e : CancellationException ) {
file . delete ( )
throw e
}
}
return file
return file
}
}
@ -461,8 +492,9 @@ class DownloadWorker @AssistedInject constructor(
private companion object {
private companion object {
const val MAX _FAILSAFE _ATTEMPTS = 2
const val MAX _FAILSAFE _ATTEMPTS = 2
const val MAX _PAGES _PARALLELISM = 4
const val DOWNLOAD _ERROR _DELAY = 500L
const val DOWNLOAD _ERROR _DELAY = 500L
const val SLOWDOWN _DELAY = 1 00L
const val SLOWDOWN _DELAY = 2 00L
const val MANGA _ID = " manga_id "
const val MANGA _ID = " manga_id "
const val CHAPTERS _IDS = " chapters "
const val CHAPTERS _IDS = " chapters "
const val TAG = " download "
const val TAG = " download "