mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-14 08:14:34 -05:00
Switch to a new Ktlint Formatter (#705)
* Switch to new Ktlint plugin * Add ktlintCheck to PR builds * Run formatter * Put ktlint version in libs toml * Fix lint * Use Zip4Java from libs.toml
This commit is contained in:
@@ -57,16 +57,16 @@ object DownloadManager {
|
||||
private val downloadQueue = CopyOnWriteArrayList<DownloadChapter>()
|
||||
private val downloaders = ConcurrentHashMap<String, Downloader>()
|
||||
|
||||
private const val downloadQueueKey = "downloadQueueKey"
|
||||
private const val DOWNLOAD_QUEUE_KEY = "downloadQueueKey"
|
||||
private val sharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences(DownloadManager::class.jvmName, Context.MODE_PRIVATE)
|
||||
|
||||
private fun loadDownloadQueue(): List<Int> {
|
||||
return sharedPreferences.getStringSet(downloadQueueKey, emptySet())?.mapNotNull { it.toInt() } ?: emptyList()
|
||||
return sharedPreferences.getStringSet(DOWNLOAD_QUEUE_KEY, emptySet())?.mapNotNull { it.toInt() } ?: emptyList()
|
||||
}
|
||||
|
||||
private fun saveDownloadQueue() {
|
||||
sharedPreferences.edit().putStringSet(downloadQueueKey, downloadQueue.map { it.chapter.id.toString() }.toSet())
|
||||
sharedPreferences.edit().putStringSet(DOWNLOAD_QUEUE_KEY, downloadQueue.map { it.chapter.id.toString() }.toSet())
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -95,30 +95,32 @@ object DownloadManager {
|
||||
|
||||
fun notifyClient(ctx: WsContext) {
|
||||
ctx.send(
|
||||
getStatus()
|
||||
getStatus(),
|
||||
)
|
||||
}
|
||||
|
||||
fun handleRequest(ctx: WsMessageContext) {
|
||||
when (ctx.message()) {
|
||||
"STATUS" -> notifyClient(ctx)
|
||||
else -> ctx.send(
|
||||
"""
|
||||
else ->
|
||||
ctx.send(
|
||||
"""
|
||||
|Invalid command.
|
||||
|Supported commands are:
|
||||
| - STATUS
|
||||
| sends the current download status
|
||||
|
|
||||
""".trimMargin()
|
||||
)
|
||||
""".trimMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val notifyFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
val status = notifyFlow.sample(1.seconds)
|
||||
.onStart { emit(Unit) }
|
||||
.map { getStatus() }
|
||||
val status =
|
||||
notifyFlow.sample(1.seconds)
|
||||
.onStart { emit(Unit) }
|
||||
.map { getStatus() }
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
@@ -129,6 +131,7 @@ object DownloadManager {
|
||||
}
|
||||
|
||||
private val saveQueueFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
init {
|
||||
saveQueueFlow.onEach { saveDownloadQueue() }.launchIn(scope)
|
||||
}
|
||||
@@ -159,11 +162,12 @@ object DownloadManager {
|
||||
} else {
|
||||
Status.Started
|
||||
},
|
||||
downloadQueue.toList()
|
||||
downloadQueue.toList(),
|
||||
)
|
||||
}
|
||||
|
||||
private val downloaderWatch = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
init {
|
||||
serverConfig.subscribeTo(serverConfig.maxSourcesInParallel, { maxSourcesInParallel ->
|
||||
val runningDownloaders = downloaders.values.filter { it.isActive }
|
||||
@@ -186,14 +190,18 @@ object DownloadManager {
|
||||
val runningDownloaders = downloaders.values.filter { it.isActive }
|
||||
val availableDownloads = downloadQueue.filter { it.state != Error }
|
||||
|
||||
logger.info { "Running: ${runningDownloaders.size}, Queued: ${availableDownloads.size}, Failed: ${downloadQueue.size - availableDownloads.size}" }
|
||||
logger.info {
|
||||
"Running: ${runningDownloaders.size}, " +
|
||||
"Queued: ${availableDownloads.size}, " +
|
||||
"Failed: ${downloadQueue.size - availableDownloads.size}"
|
||||
}
|
||||
|
||||
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) {
|
||||
availableDownloads.asSequence()
|
||||
.map { it.manga.sourceId }
|
||||
.distinct()
|
||||
.minus(
|
||||
runningDownloaders.map { it.sourceId }.toSet()
|
||||
runningDownloaders.map { it.sourceId }.toSet(),
|
||||
)
|
||||
.take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size)
|
||||
.map { getDownloader(it) }
|
||||
@@ -212,24 +220,29 @@ object DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDownloader(sourceId: String) = downloaders.getOrPut(sourceId) {
|
||||
Downloader(
|
||||
scope = scope,
|
||||
sourceId = sourceId,
|
||||
downloadQueue = downloadQueue,
|
||||
notifier = ::notifyAllClients,
|
||||
onComplete = ::refreshDownloaders,
|
||||
onDownloadFinished = ::triggerSaveDownloadQueue
|
||||
)
|
||||
}
|
||||
|
||||
fun enqueueWithChapterIndex(mangaId: Int, chapterIndex: Int) {
|
||||
val chapter = transaction {
|
||||
ChapterTable
|
||||
.slice(ChapterTable.id)
|
||||
.select { ChapterTable.manga.eq(mangaId) and ChapterTable.sourceOrder.eq(chapterIndex) }
|
||||
.first()
|
||||
private fun getDownloader(sourceId: String) =
|
||||
downloaders.getOrPut(sourceId) {
|
||||
Downloader(
|
||||
scope = scope,
|
||||
sourceId = sourceId,
|
||||
downloadQueue = downloadQueue,
|
||||
notifier = ::notifyAllClients,
|
||||
onComplete = ::refreshDownloaders,
|
||||
onDownloadFinished = ::triggerSaveDownloadQueue,
|
||||
)
|
||||
}
|
||||
|
||||
fun enqueueWithChapterIndex(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
) {
|
||||
val chapter =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.slice(ChapterTable.id)
|
||||
.select { ChapterTable.manga.eq(mangaId) and ChapterTable.sourceOrder.eq(chapterIndex) }
|
||||
.first()
|
||||
}
|
||||
enqueue(EnqueueInput(chapterIds = listOf(chapter[ChapterTable.id].value)))
|
||||
}
|
||||
|
||||
@@ -237,35 +250,38 @@ object DownloadManager {
|
||||
// Input might have additional formats in the future, such as "All for mangaID" or "Unread for categoryID"
|
||||
// Having this input format is just future-proofing
|
||||
data class EnqueueInput(
|
||||
val chapterIds: List<Int>?
|
||||
val chapterIds: List<Int>?,
|
||||
)
|
||||
|
||||
fun enqueue(input: EnqueueInput) {
|
||||
if (input.chapterIds.isNullOrEmpty()) return
|
||||
|
||||
val chapters = transaction {
|
||||
(ChapterTable innerJoin MangaTable)
|
||||
.select { ChapterTable.id inList input.chapterIds }
|
||||
.orderBy(ChapterTable.manga)
|
||||
.orderBy(ChapterTable.sourceOrder)
|
||||
.toList()
|
||||
}
|
||||
|
||||
val mangas = transaction {
|
||||
chapters.distinctBy { chapter -> chapter[MangaTable.id] }
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
val inputPairs = transaction {
|
||||
chapters.map {
|
||||
Pair(
|
||||
// this should be safe because mangas is created above from chapters
|
||||
mangas[it[ChapterTable.manga].value]!!,
|
||||
ChapterTable.toDataClass(it)
|
||||
)
|
||||
val chapters =
|
||||
transaction {
|
||||
(ChapterTable innerJoin MangaTable)
|
||||
.select { ChapterTable.id inList input.chapterIds }
|
||||
.orderBy(ChapterTable.manga)
|
||||
.orderBy(ChapterTable.sourceOrder)
|
||||
.toList()
|
||||
}
|
||||
|
||||
val mangas =
|
||||
transaction {
|
||||
chapters.distinctBy { chapter -> chapter[MangaTable.id] }
|
||||
.map { MangaTable.toDataClass(it) }
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
val inputPairs =
|
||||
transaction {
|
||||
chapters.map {
|
||||
Pair(
|
||||
// this should be safe because mangas is created above from chapters
|
||||
mangas[it[ChapterTable.manga].value]!!,
|
||||
ChapterTable.toDataClass(it),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addMultipleToQueue(inputPairs)
|
||||
}
|
||||
@@ -289,17 +305,21 @@ object DownloadManager {
|
||||
* Tries to add chapter to queue.
|
||||
* If chapter is added, returns the created DownloadChapter, otherwise returns null
|
||||
*/
|
||||
private fun addToQueue(manga: MangaDataClass, chapter: ChapterDataClass): DownloadChapter? {
|
||||
private fun addToQueue(
|
||||
manga: MangaDataClass,
|
||||
chapter: ChapterDataClass,
|
||||
): DownloadChapter? {
|
||||
val downloadChapter = downloadQueue.firstOrNull { it.mangaId == manga.id && it.chapterIndex == chapter.index }
|
||||
|
||||
val addToQueue = downloadChapter == null
|
||||
if (addToQueue) {
|
||||
val newDownloadChapter = DownloadChapter(
|
||||
chapter.index,
|
||||
manga.id,
|
||||
chapter,
|
||||
manga
|
||||
)
|
||||
val newDownloadChapter =
|
||||
DownloadChapter(
|
||||
chapter.index,
|
||||
manga.id,
|
||||
chapter,
|
||||
manga,
|
||||
)
|
||||
downloadQueue.add(newDownloadChapter)
|
||||
triggerSaveDownloadQueue()
|
||||
logger.debug { "Added chapter ${chapter.id} to download queue ($newDownloadChapter)" }
|
||||
@@ -325,11 +345,17 @@ object DownloadManager {
|
||||
dequeue(downloadQueue.filter { it.chapter.id in input.chapterIds }.toSet())
|
||||
}
|
||||
|
||||
fun dequeue(chapterIndex: Int, mangaId: Int) {
|
||||
fun dequeue(
|
||||
chapterIndex: Int,
|
||||
mangaId: Int,
|
||||
) {
|
||||
dequeue(downloadQueue.filter { it.mangaId == mangaId && it.chapterIndex == chapterIndex }.toSet())
|
||||
}
|
||||
|
||||
fun dequeue(mangaIds: List<Int>, chaptersToIgnore: List<Int> = emptyList()) {
|
||||
fun dequeue(
|
||||
mangaIds: List<Int>,
|
||||
chaptersToIgnore: List<Int> = emptyList(),
|
||||
) {
|
||||
dequeue(downloadQueue.filter { it.mangaId in mangaIds && it.chapter.id !in chaptersToIgnore }.toSet())
|
||||
}
|
||||
|
||||
@@ -342,21 +368,33 @@ object DownloadManager {
|
||||
notifyAllClients()
|
||||
}
|
||||
|
||||
fun reorder(chapterIndex: Int, mangaId: Int, to: Int) {
|
||||
val download = downloadQueue.find { it.mangaId == mangaId && it.chapterIndex == chapterIndex }
|
||||
?: return
|
||||
fun reorder(
|
||||
chapterIndex: Int,
|
||||
mangaId: Int,
|
||||
to: Int,
|
||||
) {
|
||||
val download =
|
||||
downloadQueue.find { it.mangaId == mangaId && it.chapterIndex == chapterIndex }
|
||||
?: return
|
||||
|
||||
reorder(download, to)
|
||||
}
|
||||
|
||||
fun reorder(chapterId: Int, to: Int) {
|
||||
val download = downloadQueue.find { it.chapter.id == chapterId }
|
||||
?: return
|
||||
fun reorder(
|
||||
chapterId: Int,
|
||||
to: Int,
|
||||
) {
|
||||
val download =
|
||||
downloadQueue.find { it.chapter.id == chapterId }
|
||||
?: return
|
||||
|
||||
reorder(download, to)
|
||||
}
|
||||
|
||||
private fun reorder(download: DownloadChapter, to: Int) {
|
||||
private fun reorder(
|
||||
download: DownloadChapter,
|
||||
to: Int,
|
||||
) {
|
||||
require(to >= 0) { "'to' must be over or equal to 0" }
|
||||
|
||||
logger.debug { "reorder download $download from ${downloadQueue.indexOf(download)} to $to" }
|
||||
@@ -400,5 +438,5 @@ object DownloadManager {
|
||||
enum class DownloaderState(val state: Int) {
|
||||
Stopped(0),
|
||||
Running(1),
|
||||
Paused(2)
|
||||
Paused(2),
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class Downloader(
|
||||
private val downloadQueue: CopyOnWriteArrayList<DownloadChapter>,
|
||||
private val notifier: (immediate: Boolean) -> Unit,
|
||||
private val onComplete: () -> Unit,
|
||||
private val onDownloadFinished: () -> Unit
|
||||
private val onDownloadFinished: () -> Unit,
|
||||
) {
|
||||
private val logger = KotlinLogging.logger("${Downloader::class.java.name} source($sourceId)")
|
||||
|
||||
@@ -45,9 +45,13 @@ class Downloader(
|
||||
get() = downloadQueue.filter { it.manga.sourceId == sourceId }
|
||||
|
||||
class StopDownloadException : Exception("Cancelled download")
|
||||
|
||||
class PauseDownloadException : Exception("Pause download")
|
||||
|
||||
private suspend fun step(download: DownloadChapter?, immediate: Boolean) {
|
||||
private suspend fun step(
|
||||
download: DownloadChapter?,
|
||||
immediate: Boolean,
|
||||
) {
|
||||
notifier(immediate)
|
||||
currentCoroutineContext().ensureActive()
|
||||
if (download != null && download != availableSourceDownloads.firstOrNull { it.state != Error }) {
|
||||
@@ -64,16 +68,17 @@ class Downloader(
|
||||
|
||||
fun start() {
|
||||
if (!isActive) {
|
||||
job = scope.launch {
|
||||
run()
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion {
|
||||
if (it !is CancellationException) {
|
||||
logger.debug { "completed" }
|
||||
onComplete()
|
||||
job =
|
||||
scope.launch {
|
||||
run()
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion {
|
||||
if (it !is CancellationException) {
|
||||
logger.debug { "completed" }
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug { "started" }
|
||||
notifier(false)
|
||||
}
|
||||
@@ -84,7 +89,10 @@ class Downloader(
|
||||
logger.debug { "stopped" }
|
||||
}
|
||||
|
||||
private suspend fun finishDownload(logger: KLogger, download: DownloadChapter) {
|
||||
private suspend fun finishDownload(
|
||||
logger: KLogger,
|
||||
download: DownloadChapter,
|
||||
) {
|
||||
downloadQueue.removeIf { it.mangaId == download.mangaId && it.chapterIndex == download.chapterIndex }
|
||||
step(null, false)
|
||||
logger.debug { "finished" }
|
||||
@@ -93,9 +101,10 @@ class Downloader(
|
||||
|
||||
private suspend fun run() {
|
||||
while (downloadQueue.isNotEmpty() && currentCoroutineContext().isActive) {
|
||||
val download = availableSourceDownloads.firstOrNull {
|
||||
(it.state == Queued || it.state == Finished || (it.state == Error && it.tries < 3)) // 3 re-tries per download
|
||||
} ?: break
|
||||
val download =
|
||||
availableSourceDownloads.firstOrNull {
|
||||
(it.state == Queued || it.state == Finished || (it.state == Error && it.tries < 3)) // 3 re-tries per download
|
||||
} ?: break
|
||||
|
||||
val logContext = "${logger.name} - downloadChapter($download))"
|
||||
val downloadLogger = KotlinLogging.logger(logContext)
|
||||
@@ -120,7 +129,9 @@ class Downloader(
|
||||
ChapterDownloadHelper.download(download.mangaId, download.chapter.id, download, scope, this::step)
|
||||
download.state = Finished
|
||||
transaction {
|
||||
ChapterTable.update({ (ChapterTable.manga eq download.mangaId) and (ChapterTable.sourceOrder eq download.chapterIndex) }) {
|
||||
ChapterTable.update(
|
||||
{ (ChapterTable.manga eq download.mangaId) and (ChapterTable.sourceOrder eq download.chapterIndex) },
|
||||
) {
|
||||
it[isDownloaded] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : Dow
|
||||
open suspend fun downloadImpl(
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
val pageCount = download.chapter.pageCount
|
||||
val chapterDir = getChapterCachePath(mangaId, chapterId)
|
||||
@@ -42,16 +42,17 @@ abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : Dow
|
||||
Page.getPageImage(
|
||||
mangaId = download.mangaId,
|
||||
chapterIndex = download.chapterIndex,
|
||||
index = pageNum
|
||||
index = pageNum,
|
||||
) { flow ->
|
||||
pageProgressJob = flow
|
||||
.sample(100)
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount
|
||||
step(null, false) // don't throw on canceled download here since we can't do anything
|
||||
}
|
||||
.launchIn(scope)
|
||||
pageProgressJob =
|
||||
flow
|
||||
.sample(100)
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount
|
||||
step(null, false) // don't throw on canceled download here since we can't do anything
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
} finally {
|
||||
// always cancel the page progress job even if it throws an exception to avoid memory leaks
|
||||
|
||||
@@ -13,7 +13,11 @@ fun interface FileDownload0Args : FileDownload {
|
||||
}
|
||||
|
||||
fun interface FileDownload3Args<A, B, C> : FileDownload {
|
||||
suspend fun execute(a: A, b: B, c: C): Boolean
|
||||
suspend fun execute(
|
||||
a: A,
|
||||
b: B,
|
||||
c: C,
|
||||
): Boolean
|
||||
|
||||
override suspend fun executeDownload(vararg args: Any): Boolean {
|
||||
return execute(args[0] as A, args[1] as B, args[2] as C)
|
||||
|
||||
@@ -28,7 +28,7 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mang
|
||||
override suspend fun downloadImpl(
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
val mangaDownloadFolder = File(getMangaDownloadDir(mangaId))
|
||||
val outputFile = File(getChapterCbzPath(mangaId, chapterId))
|
||||
@@ -71,7 +71,10 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mang
|
||||
return false
|
||||
}
|
||||
|
||||
private fun handleExistingCbzFile(cbzFile: File, chapterFolder: File) {
|
||||
private fun handleExistingCbzFile(
|
||||
cbzFile: File,
|
||||
chapterFolder: File,
|
||||
) {
|
||||
if (!chapterFolder.exists()) chapterFolder.mkdirs()
|
||||
ZipArchiveInputStream(cbzFile.inputStream()).use { zipInputStream ->
|
||||
var zipEntry = zipInputStream.nextEntry
|
||||
|
||||
@@ -25,7 +25,7 @@ class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(manga
|
||||
override suspend fun downloadImpl(
|
||||
download: DownloadChapter,
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit,
|
||||
): Boolean {
|
||||
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
|
||||
val folder = File(chapterDir)
|
||||
@@ -51,10 +51,14 @@ class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(manga
|
||||
return File(chapterDir).deleteRecursively()
|
||||
}
|
||||
|
||||
private fun isExistingFile(folder: File, fileName: String): Boolean {
|
||||
val existingFile = folder.listFiles { file ->
|
||||
file.isFile && file.name.startsWith(fileName)
|
||||
}?.firstOrNull()
|
||||
private fun isExistingFile(
|
||||
folder: File,
|
||||
fileName: String,
|
||||
): Boolean {
|
||||
val existingFile =
|
||||
folder.listFiles { file ->
|
||||
file.isFile && file.name.startsWith(fileName)
|
||||
}?.firstOrNull()
|
||||
return existingFile?.exists() == true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class DownloadChapter(
|
||||
var manga: MangaDataClass,
|
||||
var state: DownloadState = Queued,
|
||||
var progress: Float = 0f,
|
||||
var tries: Int = 0
|
||||
var tries: Int = 0,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "${manga.title} ($mangaId) - ${chapter.name} (${chapter.id}) | state= $state, tries= $tries, progress= $progress"
|
||||
|
||||
@@ -11,5 +11,5 @@ enum class DownloadState(val state: Int) {
|
||||
Queued(0),
|
||||
Downloading(1),
|
||||
Finished(2),
|
||||
Error(3)
|
||||
Error(3),
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ package suwayomi.tachidesk.manga.impl.download.model
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
enum class Status {
|
||||
Stopped, Started;
|
||||
Stopped,
|
||||
Started,
|
||||
}
|
||||
|
||||
data class DownloadStatus(
|
||||
val status: Status,
|
||||
val queue: List<DownloadChapter>
|
||||
val queue: List<DownloadChapter>,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user