[skip ci] Formatting

This commit is contained in:
Syer10
2024-09-03 21:37:18 -04:00
parent e968a2195a
commit 6c1fbfa63b
220 changed files with 2493 additions and 2519 deletions

View File

@@ -36,10 +36,11 @@ object Category {
return transaction {
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null) {
val newCategoryId =
CategoryTable.insertAndGetId {
it[CategoryTable.name] = name
it[CategoryTable.order] = Int.MAX_VALUE
}.value
CategoryTable
.insertAndGetId {
it[CategoryTable.name] = name
it[CategoryTable.order] = Int.MAX_VALUE
}.value
normalizeCategories()
@@ -60,7 +61,8 @@ object Category {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (
categoryId != DEFAULT_CATEGORY_ID && name != null &&
categoryId != DEFAULT_CATEGORY_ID &&
name != null &&
!name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)
) {
it[CategoryTable.name] = name
@@ -82,9 +84,11 @@ object Category {
if (from == 0 || to == 0) return
transaction {
val categories =
CategoryTable.select {
CategoryTable.id neq DEFAULT_CATEGORY_ID
}.orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
CategoryTable
.select {
CategoryTable.id neq DEFAULT_CATEGORY_ID
}.orderBy(CategoryTable.order to SortOrder.ASC)
.toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
@@ -106,7 +110,8 @@ object Category {
/** make sure category order numbers starts from 1 and is consecutive */
fun normalizeCategories() {
transaction {
CategoryTable.selectAll()
CategoryTable
.selectAll()
.orderBy(CategoryTable.order to SortOrder.ASC)
.sortedWith(compareBy({ it[CategoryTable.id].value != 0 }, { it[CategoryTable.order] }))
.forEachIndexed { index, cat ->
@@ -130,9 +135,10 @@ object Category {
const val DEFAULT_CATEGORY_ID = 0
const val DEFAULT_CATEGORY_NAME = "Default"
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll()
fun getCategoryList(): List<CategoryDataClass> =
transaction {
CategoryTable
.selectAll()
.orderBy(CategoryTable.order to SortOrder.ASC)
.let {
if (needsDefaultCategory()) {
@@ -140,41 +146,39 @@ object Category {
} else {
it.andWhere { CategoryTable.id neq DEFAULT_CATEGORY_ID }
}
}
.map {
}.map {
CategoryTable.toDataClass(it)
}
}
}
fun getCategoryById(categoryId: Int): CategoryDataClass? {
return transaction {
fun getCategoryById(categoryId: Int): CategoryDataClass? =
transaction {
CategoryTable.select { CategoryTable.id eq categoryId }.firstOrNull()?.let {
CategoryTable.toDataClass(it)
}
}
}
fun getCategorySize(categoryId: Int): Int {
return transaction {
fun getCategorySize(categoryId: Int): Int =
transaction {
if (categoryId == DEFAULT_CATEGORY_ID) {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
} else {
CategoryMangaTable.leftJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }
CategoryMangaTable
.leftJoin(MangaTable)
.select { CategoryMangaTable.category eq categoryId }
.andWhere { MangaTable.inLibrary eq true }
}.count().toInt()
}
}
fun getCategoryMetaMap(categoryId: Int): Map<String, String> {
return transaction {
CategoryMetaTable.select { CategoryMetaTable.ref eq categoryId }
fun getCategoryMetaMap(categoryId: Int): Map<String, String> =
transaction {
CategoryMetaTable
.select { CategoryMetaTable.ref eq categoryId }
.associate { it[CategoryMetaTable.key] to it[CategoryMetaTable.value] }
}
}
fun modifyMeta(
categoryId: Int,

View File

@@ -38,9 +38,10 @@ object CategoryManga {
if (categoryId == DEFAULT_CATEGORY_ID) return
fun notAlreadyInCategory() =
CategoryMangaTable.select {
(CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId)
}.isEmpty()
CategoryMangaTable
.select {
(CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId)
}.isEmpty()
transaction {
if (notAlreadyInCategory()) {
@@ -69,15 +70,17 @@ object CategoryManga {
// Select the required columns from the MangaTable and add the aggregate functions to compute unread, download, and chapter counts
val unreadCount =
wrapAsExpression<Long>(
ChapterTable.slice(
ChapterTable.id.count(),
).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id)),
ChapterTable
.slice(
ChapterTable.id.count(),
).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id)),
)
val downloadedCount =
wrapAsExpression<Long>(
ChapterTable.slice(
ChapterTable.id.count(),
).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id)),
ChapterTable
.slice(
ChapterTable.id.count(),
).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id)),
)
val chapterCount = ChapterTable.id.count().alias("chapter_count")
@@ -119,20 +122,23 @@ object CategoryManga {
/**
* list of categories that a manga belongs to
*/
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select {
CategoryMangaTable.manga eq mangaId
}.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> =
transaction {
CategoryMangaTable
.innerJoin(CategoryTable)
.select {
CategoryMangaTable.manga eq mangaId
}.orderBy(CategoryTable.order to SortOrder.ASC)
.map {
CategoryTable.toDataClass(it)
}
}
}
fun getMangasCategories(mangaIDs: List<Int>): Map<Int, List<CategoryDataClass>> {
return buildMap {
fun getMangasCategories(mangaIDs: List<Int>): Map<Int, List<CategoryDataClass>> =
buildMap {
transaction {
CategoryMangaTable.innerJoin(CategoryTable)
CategoryMangaTable
.innerJoin(CategoryTable)
.select { CategoryMangaTable.manga inList mangaIDs }
.groupBy { it[CategoryMangaTable.manga] }
.forEach {
@@ -143,5 +149,4 @@ object CategoryManga {
}
}
}
}
}

View File

@@ -50,14 +50,13 @@ import java.util.TreeSet
import java.util.concurrent.TimeUnit
import kotlin.math.max
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> {
return groupBy { it.chapterNumber }
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> =
groupBy { it.chapterNumber }
.map { (_, chapters) ->
chapters.find { it.id == currentChapter.id }
?: chapters.find { it.scanlator == currentChapter.scanlator }
?: chapters.first()
}
}
object Chapter {
private val logger = KotlinLogging.logger { }
@@ -66,12 +65,13 @@ object Chapter {
suspend fun getChapterList(
mangaId: Int,
onlineFetch: Boolean = false,
): List<ChapterDataClass> {
return if (onlineFetch) {
): List<ChapterDataClass> =
if (onlineFetch) {
getSourceChapters(mangaId)
} else {
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
ChapterTable
.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map {
ChapterTable.toDataClass(it)
@@ -80,18 +80,16 @@ object Chapter {
getSourceChapters(mangaId)
}
}
}
fun getCountOfMangaChapters(mangaId: Int): Int {
return transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count().toInt() }
}
fun getCountOfMangaChapters(mangaId: Int): Int = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count().toInt() }
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val chapterList = fetchChapterList(mangaId)
val dbChapterMap =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
ChapterTable
.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it })
}
@@ -126,7 +124,8 @@ object Chapter {
}
val map: Cache<Int, Mutex> =
CacheBuilder.newBuilder()
CacheBuilder
.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build()
@@ -173,7 +172,8 @@ object Chapter {
val chaptersInDb =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
ChapterTable
.select { ChapterTable.manga eq mangaId }
.map { ChapterTable.toDataClass(it) }
.toList()
}
@@ -259,39 +259,40 @@ object Chapter {
transaction {
if (chaptersToInsert.isNotEmpty()) {
ChapterTable.batchInsert(chaptersToInsert) { chapter ->
this[ChapterTable.url] = chapter.url
this[ChapterTable.name] = chapter.name
this[ChapterTable.date_upload] = chapter.uploadDate
this[ChapterTable.chapter_number] = chapter.chapterNumber
this[ChapterTable.scanlator] = chapter.scanlator
this[ChapterTable.sourceOrder] = chapter.index
this[ChapterTable.fetchedAt] = chapter.fetchedAt
this[ChapterTable.manga] = chapter.mangaId
this[ChapterTable.realUrl] = chapter.realUrl
this[ChapterTable.isRead] = false
this[ChapterTable.isBookmarked] = false
this[ChapterTable.isDownloaded] = false
ChapterTable
.batchInsert(chaptersToInsert) { chapter ->
this[ChapterTable.url] = chapter.url
this[ChapterTable.name] = chapter.name
this[ChapterTable.date_upload] = chapter.uploadDate
this[ChapterTable.chapter_number] = chapter.chapterNumber
this[ChapterTable.scanlator] = chapter.scanlator
this[ChapterTable.sourceOrder] = chapter.index
this[ChapterTable.fetchedAt] = chapter.fetchedAt
this[ChapterTable.manga] = chapter.mangaId
this[ChapterTable.realUrl] = chapter.realUrl
this[ChapterTable.isRead] = false
this[ChapterTable.isBookmarked] = false
this[ChapterTable.isDownloaded] = false
// is recognized chapter number
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
// is recognized chapter number
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
// only preserve download status for chapters of the same scanlator, otherwise,
// the downloaded files won't be found anyway
val downloadedChapterInfo = deletedDownloadedChapterNumberInfoMap[chapter.chapterNumber]
val pageCount = downloadedChapterInfo?.get(chapter.scanlator)
if (pageCount != null) {
this[ChapterTable.isDownloaded] = true
this[ChapterTable.pageCount] = pageCount
// only preserve download status for chapters of the same scanlator, otherwise,
// the downloaded files won't be found anyway
val downloadedChapterInfo = deletedDownloadedChapterNumberInfoMap[chapter.chapterNumber]
val pageCount = downloadedChapterInfo?.get(chapter.scanlator)
if (pageCount != null) {
this[ChapterTable.isDownloaded] = true
this[ChapterTable.pageCount] = pageCount
}
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
this[ChapterTable.fetchedAt] = it
}
}
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
this[ChapterTable.fetchedAt] = it
}
}
}.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) }
}.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) }
}
if (chaptersToUpdate.isNotEmpty()) {
@@ -531,7 +532,8 @@ object Chapter {
if (isRead == true) {
val mangaIds =
transaction {
ChapterTable.select { condition }
ChapterTable
.select { condition }
.map { it[ChapterTable.manga].value }
.toSet()
}
@@ -539,21 +541,21 @@ object Chapter {
}
}
fun getChaptersMetaMaps(chapterIds: List<EntityID<Int>>): Map<EntityID<Int>, Map<String, String>> {
return transaction {
ChapterMetaTable.select { ChapterMetaTable.ref inList chapterIds }
fun getChaptersMetaMaps(chapterIds: List<EntityID<Int>>): Map<EntityID<Int>, Map<String, String>> =
transaction {
ChapterMetaTable
.select { ChapterMetaTable.ref inList chapterIds }
.groupBy { it[ChapterMetaTable.ref] }
.mapValues { it.value.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] } }
.withDefault { emptyMap<String, String>() }
}
}
fun getChapterMetaMap(chapter: EntityID<Int>): Map<String, String> {
return transaction {
ChapterMetaTable.select { ChapterMetaTable.ref eq chapter }
fun getChapterMetaMap(chapter: EntityID<Int>): Map<String, String> =
transaction {
ChapterMetaTable
.select { ChapterMetaTable.ref eq chapter }
.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] }
}
}
fun modifyChapterMeta(
mangaId: Int,
@@ -563,8 +565,10 @@ object Chapter {
) {
transaction {
val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id].value
ChapterTable
.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id]
.value
modifyChapterMeta(chapterId, key, value)
}
}
@@ -576,7 +580,8 @@ object Chapter {
) {
transaction {
val meta =
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
ChapterMetaTable
.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
.firstOrNull()
if (meta == null) {
@@ -599,8 +604,10 @@ object Chapter {
) {
transaction {
val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id].value
ChapterTable
.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id]
.value
ChapterDownloadHelper.delete(mangaId, chapterId)
@@ -619,7 +626,8 @@ object Chapter {
} else if (input.chapterIndexes != null && mangaId != null) {
transaction {
val chapterIds =
ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
ChapterTable
.slice(ChapterTable.manga, ChapterTable.id)
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
.map { row ->
val chapterId = row[ChapterTable.id].value
@@ -637,7 +645,8 @@ object Chapter {
fun deleteChapters(chapterIds: List<Int>) {
transaction {
ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
ChapterTable
.slice(ChapterTable.manga, ChapterTable.id)
.select { ChapterTable.id inList chapterIds }
.forEach { row ->
val chapterMangaId = row[ChapterTable.manga].value
@@ -651,8 +660,8 @@ object Chapter {
}
}
fun getRecentChapters(pageNum: Int): PaginatedList<MangaChapterDataClass> {
return paginatedFrom(pageNum) {
fun getRecentChapters(pageNum: Int): PaginatedList<MangaChapterDataClass> =
paginatedFrom(pageNum) {
transaction {
(ChapterTable innerJoin MangaTable)
.select { (MangaTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaTable.inLibraryAt) }
@@ -665,5 +674,4 @@ object Chapter {
}
}
}
}
}

View File

@@ -16,16 +16,12 @@ object ChapterDownloadHelper {
mangaId: Int,
chapterId: Int,
index: Int,
): Pair<InputStream, String> {
return provider(mangaId, chapterId).getImage().execute(index)
}
): Pair<InputStream, String> = provider(mangaId, chapterId).getImage().execute(index)
fun delete(
mangaId: Int,
chapterId: Int,
): Boolean {
return provider(mangaId, chapterId).delete()
}
): Boolean = provider(mangaId, chapterId).delete()
suspend fun download(
mangaId: Int,
@@ -33,9 +29,7 @@ object ChapterDownloadHelper {
download: DownloadChapter,
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit,
): Boolean {
return provider(mangaId, chapterId).download().execute(download, scope, step)
}
): Boolean = provider(mangaId, chapterId).download().execute(download, scope, step)
// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
private fun provider(

View File

@@ -30,9 +30,10 @@ object Library {
if (!manga.inLibrary) {
transaction {
val defaultCategories =
CategoryTable.select {
(CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID)
}.toList()
CategoryTable
.select {
(CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID)
}.toList()
val existingCategories = CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) {

View File

@@ -64,13 +64,12 @@ object Manga {
private fun truncate(
text: String?,
maxLength: Int,
): String? {
return if (text?.length ?: 0 > maxLength) {
): String? =
if (text?.length ?: 0 > maxLength) {
text?.take(maxLength - 3) + "..."
} else {
text
}
}
suspend fun getManga(
mangaId: Int,
@@ -227,12 +226,12 @@ object Manga {
trackers = Track.getTrackRecordsByMangaId(mangaId),
)
fun getMangaMetaMap(mangaId: Int): Map<String, String> {
return transaction {
MangaMetaTable.select { MangaMetaTable.ref eq mangaId }
fun getMangaMetaMap(mangaId: Int): Map<String, String> =
transaction {
MangaMetaTable
.select { MangaMetaTable.ref eq mangaId }
.associate { it[MangaMetaTable.key] to it[MangaMetaTable.value] }
}
}
fun modifyMangaMeta(
mangaId: Int,
@@ -241,7 +240,8 @@ object Manga {
) {
transaction {
val meta =
MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
MangaMetaTable
.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
.firstOrNull()
if (meta == null) {
@@ -286,9 +286,10 @@ object Manga {
} ?: throw NullPointerException("No thumbnail found")
return try {
source.client.newCall(
GET(thumbnailUrl, source.headers, cache = CacheControl.FORCE_NETWORK),
).awaitSuccess()
source.client
.newCall(
GET(thumbnailUrl, source.headers, cache = CacheControl.FORCE_NETWORK),
).awaitSuccess()
} catch (e: HttpException) {
val tryToRefreshUrl =
!refreshUrl &&
@@ -341,9 +342,10 @@ object Manga {
val thumbnailUrl =
mangaEntry[MangaTable.thumbnail_url]
?: throw NullPointerException("No thumbnail found")
network.client.newCall(
GET(thumbnailUrl, cache = CacheControl.FORCE_NETWORK),
).await()
network.client
.newCall(
GET(thumbnailUrl, cache = CacheControl.FORCE_NETWORK),
).await()
}
else -> throw IllegalArgumentException("Unknown source")
@@ -372,19 +374,18 @@ object Manga {
clearCachedImage(applicationDirs.thumbnailDownloadsRoot, fileName)
}
fun getLatestChapter(mangaId: Int): ChapterDataClass? {
return transaction {
fun getLatestChapter(mangaId: Int): ChapterDataClass? =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.maxByOrNull { it[ChapterTable.sourceOrder] }
}?.let { ChapterTable.toDataClass(it) }
}
fun getUnreadChapters(mangaId: Int): List<ChapterDataClass> {
return transaction {
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq false) }
fun getUnreadChapters(mangaId: Int): List<ChapterDataClass> =
transaction {
ChapterTable
.select { (ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq false) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map { ChapterTable.toDataClass(it) }
}
}
fun isInIncludedDownloadCategory(
logContext: KLogger = logger,

View File

@@ -21,9 +21,7 @@ import suwayomi.tachidesk.manga.model.table.toDataClass
import java.time.Instant
object MangaList {
fun proxyThumbnailUrl(mangaId: Int): String {
return "/api/v1/manga/$mangaId/thumbnail"
}
fun proxyThumbnailUrl(mangaId: Int): String = "/api/v1/manga/$mangaId/thumbnail"
suspend fun getMangaList(
sourceId: Long,
@@ -47,41 +45,44 @@ object MangaList {
return mangasPage.processEntries(sourceId)
}
fun MangasPage.insertOrUpdate(sourceId: Long): List<Int> {
return transaction {
fun MangasPage.insertOrUpdate(sourceId: Long): List<Int> =
transaction {
val existingMangaUrlsToId =
MangaTable.select {
(MangaTable.sourceReference eq sourceId) and (MangaTable.url inList mangas.map { it.url })
}.associateBy { it[MangaTable.url] }
MangaTable
.select {
(MangaTable.sourceReference eq sourceId) and (MangaTable.url inList mangas.map { it.url })
}.associateBy { it[MangaTable.url] }
val existingMangaUrls = existingMangaUrlsToId.map { it.key }
val mangasToInsert = mangas.filter { !existingMangaUrls.contains(it.url) }
val insertedMangaUrlsToId =
MangaTable.batchInsert(mangasToInsert) {
this[MangaTable.url] = it.url
this[MangaTable.title] = it.title
MangaTable
.batchInsert(mangasToInsert) {
this[MangaTable.url] = it.url
this[MangaTable.title] = it.title
this[MangaTable.artist] = it.artist
this[MangaTable.author] = it.author
this[MangaTable.description] = it.description
this[MangaTable.genre] = it.genre
this[MangaTable.status] = it.status
this[MangaTable.thumbnail_url] = it.thumbnail_url
this[MangaTable.updateStrategy] = it.update_strategy.name
this[MangaTable.artist] = it.artist
this[MangaTable.author] = it.author
this[MangaTable.description] = it.description
this[MangaTable.genre] = it.genre
this[MangaTable.status] = it.status
this[MangaTable.thumbnail_url] = it.thumbnail_url
this[MangaTable.updateStrategy] = it.update_strategy.name
this[MangaTable.sourceReference] = sourceId
}.associate { Pair(it[MangaTable.url], it[MangaTable.id].value) }
this[MangaTable.sourceReference] = sourceId
}.associate { Pair(it[MangaTable.url], it[MangaTable.id].value) }
// delete thumbnail in case cached data still exists
insertedMangaUrlsToId.forEach { (_, id) -> Manga.clearThumbnail(id) }
val mangaToUpdate =
mangas.mapNotNull { sManga ->
existingMangaUrlsToId[sManga.url]?.let { sManga to it }
}.filterNot { (_, resultRow) ->
resultRow[MangaTable.inLibrary]
}
mangas
.mapNotNull { sManga ->
existingMangaUrlsToId[sManga.url]?.let { sManga to it }
}.filterNot { (_, resultRow) ->
resultRow[MangaTable.inLibrary]
}
if (mangaToUpdate.isNotEmpty()) {
BatchUpdateStatement(MangaTable).apply {
@@ -116,7 +117,6 @@ object MangaList {
?: throw Exception("MangaList::insertOrGet($sourceId): Something went wrong inserting browsed source mangas")
}
}
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this

View File

@@ -51,17 +51,20 @@ object Page {
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry =
transaction {
ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
ChapterTable
.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
val chapterId = chapterEntry[ChapterTable.id].value
val pageEntry =
transaction {
PageTable.select { (PageTable.chapter eq chapterId) }
PageTable
.select { (PageTable.chapter eq chapterId) }
.orderBy(PageTable.index to SortOrder.ASC)
.limit(1, index.toLong()).first()
.limit(1, index.toLong())
.first()
}
val tachiyomiPage =
Page(
@@ -114,7 +117,5 @@ object Page {
}
/** converts 0 to "001" */
fun getPageName(index: Int): String {
return String.format("%03d", index + 1)
}
fun getPageName(index: Int): String = String.format("%03d", index + 1)
}

View File

@@ -95,7 +95,10 @@ object Search {
private fun Filter.Select<*>.getValuesType(): String = values::class.java.componentType!!.simpleName
class SerializableGroup(name: String, state: List<FilterObject>) : Filter<List<FilterObject>>(name, state)
class SerializableGroup(
name: String,
state: List<FilterObject>,
) : Filter<List<FilterObject>>(name, state)
data class FilterObject(
val type: String,

View File

@@ -97,11 +97,10 @@ object Source {
/**
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> {
return getSourcePreferencesRaw(sourceId).map {
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> =
getSourcePreferencesRaw(sourceId).map {
PreferenceObject(it::class.java.simpleName, it)
}
}
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
val source = getCatalogueSourceOrStub(sourceId)

View File

@@ -4,20 +4,12 @@ import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ThumbnailFilePro
import java.io.InputStream
object ThumbnailDownloadHelper {
fun getImage(mangaId: Int): Pair<InputStream, String> {
return provider(mangaId).getImage().execute()
}
fun getImage(mangaId: Int): Pair<InputStream, String> = provider(mangaId).getImage().execute()
fun delete(mangaId: Int): Boolean {
return provider(mangaId).delete()
}
fun delete(mangaId: Int): Boolean = provider(mangaId).delete()
suspend fun download(mangaId: Int): Boolean {
return provider(mangaId).download().execute()
}
suspend fun download(mangaId: Int): Boolean = provider(mangaId).download().execute()
// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
private fun provider(mangaId: Int): ThumbnailFileProvider {
return ThumbnailFileProvider(mangaId)
}
private fun provider(mangaId: Int): ThumbnailFileProvider = ThumbnailFileProvider(mangaId)
}

View File

@@ -5,7 +5,9 @@ package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
interface Chapter : SChapter, Serializable {
interface Chapter :
SChapter,
Serializable {
var id: Long?
var manga_id: Long?

View File

@@ -36,7 +36,5 @@ class ChapterImpl : Chapter {
return id == chapter.id
}
override fun hashCode(): Int {
return url.hashCode() + id.hashCode()
}
override fun hashCode(): Int = url.hashCode() + id.hashCode()
}

View File

@@ -41,13 +41,9 @@ interface Manga : SManga {
setChapterFlags(order, CHAPTER_SORT_MASK)
}
fun sortDescending(): Boolean {
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
}
fun sortDescending(): Boolean = chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
fun getGenres(): List<String>? {
return genre?.split(", ")?.map { it.trim() }
}
fun getGenres(): List<String>? = genre?.split(", ")?.map { it.trim() }
private fun setChapterFlags(
flag: Int,

View File

@@ -63,7 +63,5 @@ open class MangaImpl : Manga {
return id == manga.id
}
override fun hashCode(): Int {
return url.hashCode() + id.hashCode()
}
override fun hashCode(): Int = url.hashCode() + id.hashCode()
}

View File

@@ -93,10 +93,15 @@ object ProtoBackupExport : ProtoBackupBase() {
}
}
val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() }
val (hour, minute) =
serverConfig.backupTime.value
.split(":")
.map { it.toInt() }
val backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59)
val backupInterval = serverConfig.backupInterval.value.days.coerceAtLeast(1.days)
val backupInterval =
serverConfig.backupInterval.value.days
.coerceAtLeast(1.days)
// trigger last backup in case the server wasn't running on the scheduled time
val lastAutomatedBackup = preferences.getLong(LAST_AUTOMATED_BACKUP_KEY, 0)
@@ -161,7 +166,10 @@ object ProtoBackupExport : ProtoBackupBase() {
val lastAccessTime = file.lastModified()
val isTTLReached =
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.value.days.coerceAtLeast(1.days).inWholeMilliseconds
System.currentTimeMillis() - lastAccessTime >=
serverConfig.backupTTL.value.days
.coerceAtLeast(1.days)
.inWholeMilliseconds
if (isTTLReached) {
file.delete()
}
@@ -185,7 +193,11 @@ object ProtoBackupExport : ProtoBackupBase() {
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
val byteStream = ByteArrayOutputStream()
byteStream.sink().gzip().buffer().use { it.write(byteArray) }
byteStream
.sink()
.gzip()
.buffer()
.use { it.write(byteArray) }
return byteStream.toByteArray().inputStream()
}
@@ -193,8 +205,8 @@ object ProtoBackupExport : ProtoBackupBase() {
private fun backupManga(
databaseManga: Query,
flags: BackupFlags,
): List<BackupManga> {
return databaseManga.map { mangaRow ->
): List<BackupManga> =
databaseManga.map { mangaRow ->
val backupManga =
BackupManga(
source = mangaRow[MangaTable.sourceReference],
@@ -216,7 +228,8 @@ object ProtoBackupExport : ProtoBackupBase() {
if (flags.includeChapters) {
val chapters =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
ChapterTable
.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map {
ChapterTable.toDataClass(it)
@@ -277,22 +290,23 @@ object ProtoBackupExport : ProtoBackupBase() {
backupManga
}
}
private fun backupCategories(): List<BackupCategory> {
return CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}.map {
BackupCategory(
it.name,
it.order,
0, // not supported in Tachidesk
)
}
}
private fun backupCategories(): List<BackupCategory> =
CategoryTable
.selectAll()
.orderBy(CategoryTable.order to SortOrder.ASC)
.map {
CategoryTable.toDataClass(it)
}.map {
BackupCategory(
it.name,
it.order,
0, // not supported in Tachidesk
)
}
private fun backupExtensionInfo(mangas: Query): List<BackupSource> {
return mangas
private fun backupExtensionInfo(mangas: Query): List<BackupSource> =
mangas
.asSequence()
.map { it[MangaTable.sourceReference] }
.distinct()
@@ -302,7 +316,5 @@ object ProtoBackupExport : ProtoBackupBase() {
sourceRow?.get(SourceTable.name) ?: "",
it,
)
}
.toList()
}
}.toList()
}

View File

@@ -74,18 +74,22 @@ object ProtoBackupImport : ProtoBackupBase() {
data object Failure : BackupRestoreState()
data class RestoringCategories(val totalManga: Int) : BackupRestoreState()
data class RestoringCategories(
val totalManga: Int,
) : BackupRestoreState()
data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState()
data class RestoringManga(
val current: Int,
val totalManga: Int,
val title: String,
) : BackupRestoreState()
}
private val backupRestoreIdToState = mutableMapOf<String, BackupRestoreState>()
val notifyFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST)
fun getRestoreState(id: String): BackupRestoreState? {
return backupRestoreIdToState[id]
}
fun getRestoreState(id: String): BackupRestoreState? = backupRestoreIdToState[id]
private fun updateRestoreState(
id: String,
@@ -131,8 +135,8 @@ object ProtoBackupImport : ProtoBackupBase() {
suspend fun restoreLegacy(
sourceStream: InputStream,
restoreId: String = "legacy",
): ValidationResult {
return backupMutex.withLock {
): ValidationResult =
backupMutex.withLock {
try {
logger.info { "restore($restoreId): restoring..." }
performRestore(restoreId, sourceStream)
@@ -151,13 +155,17 @@ object ProtoBackupImport : ProtoBackupBase() {
cleanupRestoreState(restoreId)
}
}
}
private fun performRestore(
id: String,
sourceStream: InputStream,
): ValidationResult {
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
val backupString =
sourceStream
.source()
.gzip()
.buffer()
.use { it.readByteArray() }
val backup = parser.decodeFromByteArray(BackupSerializer, backupString)
val validationResult = validate(backup)
@@ -174,7 +182,8 @@ object ProtoBackupImport : ProtoBackupBase() {
transaction {
backup.backupCategories.associate {
val dbCategory =
CategoryTable.select { CategoryTable.name eq it.name }
CategoryTable
.select { CategoryTable.name eq it.name }
.firstOrNull()
val categoryId =
dbCategory?.let { categoryResultRow ->
@@ -265,7 +274,8 @@ object ProtoBackupImport : ProtoBackupBase() {
) {
val dbManga =
transaction {
MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
MangaTable
.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
.firstOrNull()
}
@@ -274,26 +284,27 @@ object ProtoBackupImport : ProtoBackupBase() {
transaction {
// insert manga to database
val mangaId =
MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
MangaTable
.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[sourceReference] = manga.source
it[sourceReference] = manga.source
it[initialized] = manga.description != null
it[initialized] = manga.description != null
it[inLibrary] = manga.favorite
it[inLibrary] = manga.favorite
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value
// delete thumbnail in case cached data still exists
clearThumbnail(mangaId)
@@ -394,34 +405,36 @@ object ProtoBackupImport : ProtoBackupBase() {
}
val dbTrackRecordsByTrackerId =
Tracker.getTrackRecordsByMangaId(mangaId)
Tracker
.getTrackRecordsByMangaId(mangaId)
.mapNotNull { it.record?.toTrack() }
.associateBy { it.sync_id }
val (existingTracks, newTracks) =
tracks.mapNotNull { backupTrack ->
val track = backupTrack.toTrack(mangaId)
tracks
.mapNotNull { backupTrack ->
val track = backupTrack.toTrack(mangaId)
val isUnsupportedTracker = TrackerManager.getTracker(track.sync_id) == null
if (isUnsupportedTracker) {
return@mapNotNull null
}
val isUnsupportedTracker = TrackerManager.getTracker(track.sync_id) == null
if (isUnsupportedTracker) {
return@mapNotNull null
}
val dbTrack =
dbTrackRecordsByTrackerId[backupTrack.syncId]
?: // new track
return@mapNotNull track
val dbTrack =
dbTrackRecordsByTrackerId[backupTrack.syncId]
?: // new track
return@mapNotNull track
if (track.toTrackRecordDataClass().forComparison() == dbTrack.toTrackRecordDataClass().forComparison()) {
return@mapNotNull null
}
if (track.toTrackRecordDataClass().forComparison() == dbTrack.toTrackRecordDataClass().forComparison()) {
return@mapNotNull null
}
dbTrack.also {
it.media_id = track.media_id
it.library_id = track.library_id
it.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
}
}.partition { (it.id ?: -1) > 0 }
dbTrack.also {
it.media_id = track.media_id
it.library_id = track.library_id
it.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
}
}.partition { (it.id ?: -1) > 0 }
existingTracks.forEach(Tracker::updateTrackRecord)
newTracks.forEach(Tracker::insertTrackRecord)

View File

@@ -64,7 +64,12 @@ object ProtoBackupValidator {
}
fun validate(sourceStream: InputStream): ValidationResult {
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
val backupString =
sourceStream
.source()
.gzip()
.buffer()
.use { it.readByteArray() }
val backup = ProtoBackupImport.parser.decodeFromByteArray(BackupSerializer, backupString)
return validate(backup)

View File

@@ -14,10 +14,9 @@ data class Backup(
@ProtoNumber(100) var brokenBackupSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
) {
fun getSourceMap(): Map<Long, String> {
return (brokenBackupSources.map { BackupSource(it.name, it.sourceId) } + backupSources)
fun getSourceMap(): Map<Long, String> =
(brokenBackupSources.map { BackupSource(it.name, it.sourceId) } + backupSources)
.associate { it.sourceId to it.name }
}
companion object {
fun getBasename(name: String = ""): String {

View File

@@ -21,8 +21,8 @@ data class BackupChapter(
@ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Int = 0,
) {
fun toChapterImpl(): ChapterImpl {
return ChapterImpl().apply {
fun toChapterImpl(): ChapterImpl =
ChapterImpl().apply {
url = this@BackupChapter.url
name = this@BackupChapter.name
chapter_number = this@BackupChapter.chapterNumber
@@ -34,5 +34,4 @@ data class BackupChapter(
date_upload = this@BackupChapter.dateUpload
source_order = this@BackupChapter.sourceOrder
}
}
}

View File

@@ -39,8 +39,8 @@ data class BackupManga(
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
fun getMangaImpl(): MangaImpl =
MangaImpl().apply {
url = this@BackupManga.url
title = this@BackupManga.title
artist = this@BackupManga.artist
@@ -56,23 +56,20 @@ data class BackupManga(
chapter_flags = this@BackupManga.chapterFlags
update_strategy = this@BackupManga.updateStrategy
}
}
fun getChaptersImpl(): List<ChapterImpl> {
return chapters.map {
fun getChaptersImpl(): List<ChapterImpl> =
chapters.map {
it.toChapterImpl()
}
}
fun getTrackingImpl(): List<TrackImpl> {
return tracking.map {
fun getTrackingImpl(): List<TrackImpl> =
tracking.map {
it.getTrackingImpl()
}
}
companion object {
fun copyFrom(manga: Manga): BackupManga {
return BackupManga(
fun copyFrom(manga: Manga): BackupManga =
BackupManga(
url = manga.url,
title = manga.title,
artist = manga.artist,
@@ -88,6 +85,5 @@ data class BackupManga(
viewer_flags = manga.viewer_flags,
chapterFlags = manga.chapter_flags,
)
}
}
}

View File

@@ -16,11 +16,10 @@ data class BackupSource(
@ProtoNumber(2) var sourceId: Long,
) {
companion object {
fun copyFrom(source: Source): BackupSource {
return BackupSource(
fun copyFrom(source: Source): BackupSource =
BackupSource(
name = source.name,
sourceId = source.id,
)
}
}
}

View File

@@ -28,8 +28,8 @@ data class BackupTracking(
@ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(100) var mediaId: Long = 0,
) {
fun getTrackingImpl(): TrackImpl {
return TrackImpl().apply {
fun getTrackingImpl(): TrackImpl =
TrackImpl().apply {
sync_id = this@BackupTracking.syncId
media_id =
if (this@BackupTracking.mediaIdInt != 0) {
@@ -48,11 +48,10 @@ data class BackupTracking(
finished_reading_date = this@BackupTracking.finishedReadingDate
tracking_url = this@BackupTracking.trackingUrl
}
}
companion object {
fun copyFrom(track: Track): BackupTracking {
return BackupTracking(
fun copyFrom(track: Track): BackupTracking =
BackupTracking(
syncId = track.sync_id,
mediaId = track.media_id,
// forced not null so its compatible with 1.x backup system
@@ -67,6 +66,5 @@ data class BackupTracking(
finishedReadingDate = track.finished_reading_date,
trackingUrl = track.tracking_url,
)
}
}
}

View File

@@ -37,16 +37,12 @@ suspend fun getChapterDownloadReady(
return chapter.asDownloadReady()
}
suspend fun getChapterDownloadReadyById(chapterId: Int): ChapterDataClass {
return getChapterDownloadReady(chapterId = chapterId)
}
suspend fun getChapterDownloadReadyById(chapterId: Int): ChapterDataClass = getChapterDownloadReady(chapterId = chapterId)
suspend fun getChapterDownloadReadyByIndex(
chapterIndex: Int,
mangaId: Int,
): ChapterDataClass {
return getChapterDownloadReady(chapterIndex = chapterIndex, mangaId = mangaId)
}
): ChapterDataClass = getChapterDownloadReady(chapterIndex = chapterIndex, mangaId = mangaId)
private class ChapterForDownload(
optChapterId: Int? = null,
@@ -101,15 +97,16 @@ private class ChapterForDownload(
optChapterIndex: Int? = null,
optMangaId: Int? = null,
) = transaction {
ChapterTable.select {
if (optChapterId != null) {
ChapterTable.id eq optChapterId
} else if (optChapterIndex != null && optMangaId != null) {
(ChapterTable.sourceOrder eq optChapterIndex) and (ChapterTable.manga eq optMangaId)
} else {
throw Exception("'optChapterId' or 'optChapterIndex' and 'optMangaId' have to be passed")
}
}.first()
ChapterTable
.select {
if (optChapterId != null) {
ChapterTable.id eq optChapterId
} else if (optChapterIndex != null && optMangaId != null) {
(ChapterTable.sourceOrder eq optChapterIndex) and (ChapterTable.manga eq optMangaId)
} else {
throw Exception("'optChapterId' or 'optChapterIndex' and 'optMangaId' have to be passed")
}
}.first()
}
private suspend fun fetchPageList(): List<Page> {
@@ -166,12 +163,11 @@ private class ChapterForDownload(
}
}
private fun firstPageExists(): Boolean {
return try {
private fun firstPageExists(): Boolean =
try {
ChapterDownloadHelper.getImage(mangaId, chapterId, 0).first.close()
true
} catch (e: Exception) {
false
}
}
}

View File

@@ -63,12 +63,17 @@ object DownloadManager {
private val sharedPreferences =
Injekt.get<Application>().getSharedPreferences(DownloadManager::class.jvmName, Context.MODE_PRIVATE)
private fun loadDownloadQueue(): List<Int> {
return sharedPreferences.getStringSet(DOWNLOAD_QUEUE_KEY, emptySet())?.mapNotNull { it.toInt() }.orEmpty()
}
private fun loadDownloadQueue(): List<Int> =
sharedPreferences
.getStringSet(DOWNLOAD_QUEUE_KEY, emptySet())
?.mapNotNull {
it.toInt()
}.orEmpty()
private fun saveDownloadQueue() {
sharedPreferences.edit().putStringSet(DOWNLOAD_QUEUE_KEY, downloadQueue.map { it.chapter.id.toString() }.toSet())
sharedPreferences
.edit()
.putStringSet(DOWNLOAD_QUEUE_KEY, downloadQueue.map { it.chapter.id.toString() }.toSet())
.apply()
}
@@ -159,8 +164,8 @@ object DownloadManager {
}
}
private fun getStatus(): DownloadStatus {
return DownloadStatus(
private fun getStatus(): DownloadStatus =
DownloadStatus(
if (downloadQueue.none { it.state == Downloading }) {
Status.Stopped
} else {
@@ -168,7 +173,6 @@ object DownloadManager {
},
downloadQueue.toList(),
)
}
private val downloaderWatch = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@@ -201,13 +205,13 @@ object DownloadManager {
}
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) {
availableDownloads.asSequence()
availableDownloads
.asSequence()
.map { it.manga.sourceId }
.distinct()
.minus(
runningDownloaders.map { it.sourceId }.toSet(),
)
.take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size)
).take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size)
.map { getDownloader(it) }
.forEach {
it.start()
@@ -271,7 +275,8 @@ object DownloadManager {
val mangas =
transaction {
chapters.distinctBy { chapter -> chapter[MangaTable.id] }
chapters
.distinctBy { chapter -> chapter[MangaTable.id] }
.map { MangaTable.toDataClass(it) }
.associateBy { it.id }
}
@@ -420,11 +425,12 @@ object DownloadManager {
logger.debug { "stop" }
coroutineScope {
downloaders.map { (_, downloader) ->
async {
downloader.stop()
}
}.awaitAll()
downloaders
.map { (_, downloader) ->
async {
downloader.stop()
}
}.awaitAll()
}
notifyAllClients()
}
@@ -439,7 +445,9 @@ object DownloadManager {
}
}
enum class DownloaderState(val state: Int) {
enum class DownloaderState(
val state: Int,
) {
Stopped(0),
Running(1),
Paused(2),

View File

@@ -69,16 +69,17 @@ class Downloader(
fun start() {
if (!isActive) {
job =
scope.launch {
run()
}.also { job ->
job.invokeOnCompletion {
if (it !is CancellationException) {
logger.debug { "completed" }
onComplete()
scope
.launch {
run()
}.also { job ->
job.invokeOnCompletion {
if (it !is CancellationException) {
logger.debug { "completed" }
onComplete()
}
}
}
}
logger.debug { "started" }
notifier(false)
}

View File

@@ -23,12 +23,16 @@ import java.io.File
import java.io.InputStream
sealed class FileType {
data class RegularFile(val file: File) : FileType()
data class RegularFile(
val file: File,
) : FileType()
data class ZipFile(val entry: ZipArchiveEntry) : FileType()
data class ZipFile(
val entry: ZipArchiveEntry,
) : FileType()
fun getName(): String {
return when (this) {
fun getName(): String =
when (this) {
is FileType.RegularFile -> {
this.file.name
}
@@ -36,10 +40,9 @@ sealed class FileType {
this.entry.name
}
}
}
fun getExtension(): String {
return when (this) {
fun getExtension(): String =
when (this) {
is FileType.RegularFile -> {
this.file.extension
}
@@ -47,13 +50,15 @@ sealed class FileType {
this.entry.name.substringAfterLast(".")
}
}
}
}
/*
* Base class for downloaded chapter files provider, example: Folder, Archive
*/
abstract class ChaptersFilesProvider<Type : FileType>(val mangaId: Int, val chapterId: Int) : DownloadedFilesProvider {
abstract class ChaptersFilesProvider<Type : FileType>(
val mangaId: Int,
val chapterId: Int,
) : DownloadedFilesProvider {
protected abstract fun getImageFiles(): List<Type>
protected abstract fun getImageInputStream(image: Type): InputStream
@@ -71,9 +76,7 @@ abstract class ChaptersFilesProvider<Type : FileType>(val mangaId: Int, val chap
return Pair(getImageInputStream(image).buffered(), "image/$imageFileType")
}
override fun getImage(): RetrieveFile1Args<Int> {
return RetrieveFile1Args(::getImageImpl)
}
override fun getImage(): RetrieveFile1Args<Int> = RetrieveFile1Args(::getImageImpl)
/**
* Extract the existing download to the base download folder (see [getChapterDownloadPath])
@@ -110,21 +113,22 @@ abstract class ChaptersFilesProvider<Type : FileType>(val mangaId: Int, val chap
}
try {
Page.getPageImage(
mangaId = download.mangaId,
chapterIndex = download.chapterIndex,
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)
}.first.close()
Page
.getPageImage(
mangaId = download.mangaId,
chapterIndex = download.chapterIndex,
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)
}.first
.close()
} finally {
// always cancel the page progress job even if it throws an exception to avoid memory leaks
pageProgressJob?.cancel()
@@ -151,9 +155,8 @@ abstract class ChaptersFilesProvider<Type : FileType>(val mangaId: Int, val chap
return true
}
override fun download(): FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> {
return FileDownload3Args(::downloadImpl)
}
override fun download(): FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> =
FileDownload3Args(::downloadImpl)
abstract override fun delete(): Boolean
}

View File

@@ -1,5 +1,7 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider
interface DownloadedFilesProvider : FileDownloader, FileRetriever {
interface DownloadedFilesProvider :
FileDownloader,
FileRetriever {
fun delete(): Boolean
}

View File

@@ -7,9 +7,7 @@ fun interface FileDownload {
fun interface FileDownload0Args : FileDownload {
suspend fun execute(): Boolean
override suspend fun executeDownload(vararg args: Any): Boolean {
return execute()
}
override suspend fun executeDownload(vararg args: Any): Boolean = execute()
}
@Suppress("UNCHECKED_CAST")
@@ -20,9 +18,7 @@ fun interface FileDownload3Args<A, B, C> : FileDownload {
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)
}
override suspend fun executeDownload(vararg args: Any): Boolean = execute(args[0] as A, args[1] as B, args[2] as C)
}
fun interface FileDownloader {

View File

@@ -9,18 +9,14 @@ fun interface RetrieveFile {
fun interface RetrieveFile0Args : RetrieveFile {
fun execute(): Pair<InputStream, String>
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> {
return execute()
}
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> = execute()
}
@Suppress("UNCHECKED_CAST")
fun interface RetrieveFile1Args<A> : RetrieveFile {
fun execute(a: A): Pair<InputStream, String>
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> {
return execute(args[0] as A)
}
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> = execute(args[0] as A)
}
fun interface FileRetriever {

View File

@@ -21,15 +21,21 @@ import java.io.InputStream
private val applicationDirs by DI.global.instance<ApplicationDirs>()
class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<FileType.ZipFile>(mangaId, chapterId) {
class ArchiveProvider(
mangaId: Int,
chapterId: Int,
) : ChaptersFilesProvider<FileType.ZipFile>(mangaId, chapterId) {
override fun getImageFiles(): List<FileType.ZipFile> {
val zipFile = ZipFile(getChapterCbzPath(mangaId, chapterId))
val zipFile = ZipFile.builder().setFile(getChapterCbzPath(mangaId, chapterId)).get()
return zipFile.entries.toList().map { FileType.ZipFile(it) }
}
override fun getImageInputStream(image: FileType.ZipFile): InputStream {
return ZipFile(getChapterCbzPath(mangaId, chapterId)).getInputStream(image.entry)
}
override fun getImageInputStream(image: FileType.ZipFile): InputStream =
ZipFile
.builder()
.setFile(getChapterCbzPath(mangaId, chapterId))
.get()
.getInputStream(image.entry)
override fun extractExistingDownload() {
val outputFile = File(getChapterCbzPath(mangaId, chapterId))

View File

@@ -17,7 +17,10 @@ private val applicationDirs by DI.global.instance<ApplicationDirs>()
/*
* Provides downloaded files when pages were downloaded into folders
* */
class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<RegularFile>(mangaId, chapterId) {
class FolderProvider(
mangaId: Int,
chapterId: Int,
) : ChaptersFilesProvider<RegularFile>(mangaId, chapterId) {
override fun getImageFiles(): List<RegularFile> {
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
@@ -25,12 +28,14 @@ class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<Regul
throw Exception("download folder does not exist")
}
return chapterFolder.listFiles().orEmpty().toList().map(::RegularFile)
return chapterFolder
.listFiles()
.orEmpty()
.toList()
.map(::RegularFile)
}
override fun getImageInputStream(image: RegularFile): FileInputStream {
return FileInputStream(image.file)
}
override fun getImageInputStream(image: RegularFile): FileInputStream = FileInputStream(image.file)
override fun extractExistingDownload() {
// nothing to do

View File

@@ -18,7 +18,9 @@ class MissingThumbnailException : Exception("No thumbnail found")
private val applicationDirs by DI.global.instance<ApplicationDirs>()
class ThumbnailFileProvider(val mangaId: Int) : DownloadedFilesProvider {
class ThumbnailFileProvider(
val mangaId: Int,
) : DownloadedFilesProvider {
private fun getFilePath(): String? {
val thumbnailDir = applicationDirs.thumbnailDownloadsRoot
val fileName = mangaId.toString()
@@ -36,9 +38,7 @@ class ThumbnailFileProvider(val mangaId: Int) : DownloadedFilesProvider {
return getCachedImageResponse(filePath, filePathWithoutExt)
}
override fun getImage(): RetrieveFile0Args {
return RetrieveFile0Args(::getImageImpl)
}
override fun getImage(): RetrieveFile0Args = RetrieveFile0Args(::getImageImpl)
private suspend fun downloadImpl(): Boolean {
val isExistingFile = getFilePath() != null
@@ -55,9 +55,7 @@ class ThumbnailFileProvider(val mangaId: Int) : DownloadedFilesProvider {
return true
}
override fun download(): FileDownload0Args {
return FileDownload0Args(::downloadImpl)
}
override fun download(): FileDownload0Args = FileDownload0Args(::downloadImpl)
override fun delete(): Boolean {
val filePath = getFilePath()

View File

@@ -20,7 +20,6 @@ class DownloadChapter(
var progress: Float = 0f,
var tries: Int = 0,
) {
override fun toString(): String {
return "${manga.title} ($mangaId) - ${chapter.name} (${chapter.id}) | state= $state, tries= $tries, progress= $progress"
}
override fun toString(): String =
"${manga.title} ($mangaId) - ${chapter.name} (${chapter.id}) | state= $state, tries= $tries, progress= $progress"
}

View File

@@ -7,7 +7,9 @@ package suwayomi.tachidesk.manga.impl.download.model
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
enum class DownloadState(val state: Int) {
enum class DownloadState(
val state: Int,
) {
Queued(0),
Downloading(1),
Finished(2),

View File

@@ -78,8 +78,8 @@ object Extension {
suspend fun installExternalExtension(
inputStream: InputStream,
apkName: String,
): Int {
return installAPK(true) {
): Int =
installAPK(true) {
val savePath = "${applicationDirs.extensionsRoot}/$apkName"
logger.debug { "Saving apk at $apkName" }
// download apk file
@@ -92,7 +92,6 @@ object Extension {
}
savePath
}
}
suspend fun installAPK(
forceReinstall: Boolean = false,
@@ -174,7 +173,10 @@ object Extension {
else -> "all"
}
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
val extensionName =
packageInfo.applicationInfo.nonLocalizedLabel
.toString()
.substringAfter("Tachiyomi: ")
// update extension info
transaction {
@@ -277,9 +279,10 @@ object Extension {
savePath: String,
) {
val response =
network.client.newCall(
GET(url, cache = CacheControl.FORCE_NETWORK),
).await()
network.client
.newCall(
GET(url, cache = CacheControl.FORCE_NETWORK),
).await()
val downloadedFile = File(savePath)
downloadedFile.sink().buffer().use { sink ->
@@ -355,13 +358,12 @@ object Extension {
val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
return getImageResponse(cacheSaveDir, apkName) {
network.client.newCall(
GET(iconUrl, cache = CacheControl.FORCE_NETWORK),
).await()
network.client
.newCall(
GET(iconUrl, cache = CacheControl.FORCE_NETWORK),
).await()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/extension/icon/$apkName"
}
fun getExtensionIconUrl(apkName: String): String = "/api/v1/extension/icon/$apkName"
}

View File

@@ -39,13 +39,14 @@ object ExtensionsList {
// update if 60 seconds has passed or requested offline and database is empty
val extensions =
serverConfig.extensionRepos.value.map { repo ->
kotlin.runCatching {
ExtensionGithubApi.findExtensions(repo.repoUrlReplace())
}.onFailure {
logger.warn(it) {
"Failed to fetch extensions for repo: $repo"
kotlin
.runCatching {
ExtensionGithubApi.findExtensions(repo.repoUrlReplace())
}.onFailure {
logger.warn(it) {
"Failed to fetch extensions for repo: $repo"
}
}
}
}
val foundExtensions = extensions.mapNotNull { it.getOrNull() }.flatten()
updateExtensionDatabase(foundExtensions)
@@ -94,12 +95,15 @@ object ExtensionsList {
updateExtensionDatabaseMutex.withLock {
transaction {
val uniqueExtensions =
foundExtensions.groupBy { it.pkgName }.mapValues {
(_, extension) ->
extension.maxBy { it.versionCode }
}.values
foundExtensions
.groupBy { it.pkgName }
.mapValues { (_, extension) ->
extension.maxBy { it.versionCode }
}.values
val installedExtensions =
ExtensionTable.selectAll().toList()
ExtensionTable
.selectAll()
.toList()
.associateBy { it[ExtensionTable.pkgName] }
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
val extensionsToInsert = mutableListOf<OnlineExtension>()
@@ -189,7 +193,8 @@ object ExtensionsList {
// deal with obsolete extensions
val extensionsToRemove =
extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] }
extensionsToDelete
.groupBy { it[ExtensionTable.isInstalled] }
.mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } }
// not in the repo, so these extensions are obsolete
val obsoleteExtensions = extensionsToRemove[true].orEmpty()
@@ -207,8 +212,8 @@ object ExtensionsList {
}
}
private fun String.repoUrlReplace(): String {
return if (contains("github")) {
private fun String.repoUrlReplace(): String =
if (contains("github")) {
replace(repoMatchRegex) {
"https://raw.githubusercontent.com/${it.groupValues[2]}/${it.groupValues[3]}/" +
(it.groupValues.getOrNull(4)?.ifBlank { null } ?: "repo") +
@@ -218,7 +223,6 @@ object ExtensionsList {
} else {
this
}
}
private val repoMatchRegex =
(

View File

@@ -58,29 +58,27 @@ object ExtensionGithubApi {
fun getApkUrl(
repo: String,
apkName: String,
): String {
return "${repo}apk/$apkName"
}
): String = "${repo}apk/$apkName"
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
network.client
.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
originalResponse
.newBuilder()
.header("Content-Type", "application/json")
.build()
}
.build()
}.build()
}
private fun List<ExtensionJsonObject>.toExtensions(repo: String): List<OnlineExtension> {
return this
private fun List<ExtensionJsonObject>.toExtensions(repo: String): List<OnlineExtension> =
this
.filter {
val libVersion = it.version.substringBeforeLast('.').toDouble()
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
}
.map {
}.map {
OnlineExtension(
repo = repo,
name = it.name.substringAfter("Tachiyomi: "),
@@ -96,10 +94,9 @@ object ExtensionGithubApi {
iconUrl = "${repo}icon/${it.pkg}.png",
)
}
}
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<OnlineExtensionSource> {
return this.map {
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<OnlineExtensionSource> =
this.map {
OnlineExtensionSource(
name = it.name,
lang = it.lang,
@@ -107,5 +104,4 @@ object ExtensionGithubApi {
baseUrl = it.baseUrl,
)
}
}
}

View File

@@ -63,9 +63,7 @@ object Track {
tracker.logout()
}
fun proxyThumbnailUrl(trackerId: Int): String {
return "/api/v1/track/$trackerId/thumbnail"
}
fun proxyThumbnailUrl(trackerId: Int): String = "/api/v1/track/$trackerId/thumbnail"
fun getTrackerThumbnail(trackerId: Int): Pair<InputStream, String> {
val tracker = TrackerManager.getTracker(trackerId)!!
@@ -76,7 +74,8 @@ object Track {
fun getTrackRecordsByMangaId(mangaId: Int): List<MangaTrackerDataClass> {
val recordMap =
transaction {
TrackRecordTable.select { TrackRecordTable.mangaId eq mangaId }
TrackRecordTable
.select { TrackRecordTable.mangaId eq mangaId }
.map { it.toTrackRecordDataClass() }
}.associateBy { it.trackerId }
@@ -138,10 +137,12 @@ object Track {
) {
val track =
transaction {
TrackSearchTable.select {
TrackSearchTable.trackerId eq trackerId and
(TrackSearchTable.remoteId eq remoteId)
}.first().toTrack(mangaId)
TrackSearchTable
.select {
TrackSearchTable.trackerId eq trackerId and
(TrackSearchTable.remoteId eq remoteId)
}.first()
.toTrack(mangaId)
}
val tracker = TrackerManager.getTracker(trackerId)!!
@@ -160,10 +161,10 @@ object Track {
if (track.started_reading_date <= 0) {
val oldestChapter =
transaction {
ChapterTable.select {
(ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq true)
}
.orderBy(ChapterTable.lastReadAt to SortOrder.ASC)
ChapterTable
.select {
(ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq true)
}.orderBy(ChapterTable.lastReadAt to SortOrder.ASC)
.limit(1)
.firstOrNull()
}
@@ -290,14 +291,14 @@ object Track {
}
}
private fun queryMaxReadChapter(mangaId: Int): ResultRow? {
return transaction {
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq true) }
private fun queryMaxReadChapter(mangaId: Int): ResultRow? =
transaction {
ChapterTable
.select { (ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.chapter_number to SortOrder.DESC)
.limit(1)
.firstOrNull()
}
}
private suspend fun trackChapter(
mangaId: Int,
@@ -305,7 +306,8 @@ object Track {
) {
val records =
transaction {
TrackRecordTable.select { TrackRecordTable.mangaId eq mangaId }
TrackRecordTable
.select { TrackRecordTable.mangaId eq mangaId }
.toList()
}
@@ -313,7 +315,8 @@ object Track {
try {
trackChapterForTracker(it, chapterNumber)
} catch (e: Exception) {
KotlinLogging.logger("${logger.name}::trackChapter(mangaId= $mangaId, chapterNumber= $chapterNumber)")
KotlinLogging
.logger("${logger.name}::trackChapter(mangaId= $mangaId, chapterNumber= $chapterNumber)")
.error(e) { "failed due to" }
}
}
@@ -358,14 +361,14 @@ object Track {
}
}
fun upsertTrackRecord(track: Track): Int {
return transaction {
fun upsertTrackRecord(track: Track): Int =
transaction {
val existingRecord =
TrackRecordTable.select {
(TrackRecordTable.mangaId eq track.manga_id) and
(TrackRecordTable.trackerId eq track.sync_id)
}
.singleOrNull()
TrackRecordTable
.select {
(TrackRecordTable.mangaId eq track.manga_id) and
(TrackRecordTable.trackerId eq track.sync_id)
}.singleOrNull()
if (existingRecord != null) {
updateTrackRecord(track)
@@ -374,7 +377,6 @@ object Track {
insertTrackRecord(track)
}
}
}
fun updateTrackRecord(track: Track): Int =
transaction {
@@ -399,20 +401,21 @@ object Track {
fun insertTrackRecord(track: Track): Int =
transaction {
TrackRecordTable.insertAndGetId {
it[mangaId] = track.manga_id
it[trackerId] = track.sync_id
it[remoteId] = track.media_id
it[libraryId] = track.library_id
it[title] = track.title
it[lastChapterRead] = track.last_chapter_read.toDouble()
it[totalChapters] = track.total_chapters
it[status] = track.status
it[score] = track.score.toDouble()
it[remoteUrl] = track.tracking_url
it[startDate] = track.started_reading_date
it[finishDate] = track.finished_reading_date
}.value
TrackRecordTable
.insertAndGetId {
it[mangaId] = track.manga_id
it[trackerId] = track.sync_id
it[remoteId] = track.media_id
it[libraryId] = track.library_id
it[title] = track.title
it[lastChapterRead] = track.last_chapter_read.toDouble()
it[totalChapters] = track.total_chapters
it[status] = track.status
it[score] = track.score.toDouble()
it[remoteUrl] = track.tracking_url
it[startDate] = track.started_reading_date
it[finishDate] = track.finished_reading_date
}.value
}
@Serializable

View File

@@ -7,7 +7,10 @@ import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.io.IOException
abstract class Tracker(val id: Int, val name: String) {
abstract class Tracker(
val id: Int,
val name: String,
) {
val trackPreferences = TrackerPreferences
private val networkService: NetworkHelper by injectLazy()
@@ -35,9 +38,7 @@ abstract class Tracker(val id: Int, val name: String) {
abstract fun getScoreList(): List<String>
open fun indexToScore(index: Int): Float {
return index.toFloat()
}
open fun indexToScore(index: Int): Float = index.toFloat()
abstract fun displayScore(track: Track): String
@@ -55,9 +56,7 @@ abstract class Tracker(val id: Int, val name: String) {
abstract suspend fun refresh(track: Track): Track
open fun authUrl(): String? {
return null
}
open fun authUrl(): String? = null
open suspend fun authCallback(url: String) {}
@@ -87,9 +86,7 @@ abstract class Tracker(val id: Int, val name: String) {
trackPreferences.setTrackCredentials(this, username, password)
}
fun getIfAuthExpired(): Boolean {
return trackPreferences.trackAuthExpired(this)
}
fun getIfAuthExpired(): Boolean = trackPreferences.trackAuthExpired(this)
fun setAuthExpired() {
trackPreferences.setTrackTokenExpired(this)

View File

@@ -28,7 +28,8 @@ object TrackerPreferences {
password: String,
) {
logger.debug { "setTrackCredentials: id=${sync.id} username=$username" }
preferenceStore.edit()
preferenceStore
.edit()
.putString(trackUsername(sync.id), username)
.putString(trackPassword(sync.id), password)
.putBoolean(trackTokenExpired(sync.id), false)
@@ -43,12 +44,14 @@ object TrackerPreferences {
) {
logger.debug { "setTrackToken: id=${sync.id} token=$token" }
if (token == null) {
preferenceStore.edit()
preferenceStore
.edit()
.remove(trackToken(sync.id))
.putBoolean(trackTokenExpired(sync.id), false)
.apply()
} else {
preferenceStore.edit()
preferenceStore
.edit()
.putString(trackToken(sync.id), token)
.putBoolean(trackTokenExpired(sync.id), false)
.apply()
@@ -56,7 +59,8 @@ object TrackerPreferences {
}
fun setTrackTokenExpired(sync: Tracker) {
preferenceStore.edit()
preferenceStore
.edit()
.putBoolean(trackTokenExpired(sync.id), true)
.apply()
}
@@ -66,7 +70,8 @@ object TrackerPreferences {
fun setScoreType(
sync: Tracker,
scoreType: String,
) = preferenceStore.edit()
) = preferenceStore
.edit()
.putString(scoreType(sync.id), scoreType)
.apply()

View File

@@ -12,7 +12,10 @@ import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.io.IOException
class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
class Anilist(
id: Int,
) : Tracker(id, "AniList"),
DeletableTrackService {
companion object {
const val READING = 1
const val COMPLETED = 2
@@ -40,13 +43,9 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
private val logger = KotlinLogging.logger {}
override fun getLogo(): String {
return "/static/tracker/anilist.png"
}
override fun getLogo(): String = "/static/tracker/anilist.png"
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
}
override fun getStatusList(): List<Int> = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
@StringRes
override fun getStatus(status: Int): String? =
@@ -66,8 +65,8 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
override fun getCompletionStatus(): Int = COMPLETED
override fun getScoreList(): List<String> {
return when (trackPreferences.getScoreType(this)) {
override fun getScoreList(): List<String> =
when (trackPreferences.getScoreType(this)) {
// 10 point
POINT_10 -> IntRange(0, 10).map(Int::toString)
// 100 point
@@ -80,10 +79,9 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
else -> throw Exception("Unknown score type")
}
}
override fun indexToScore(index: Int): Float {
return when (trackPreferences.getScoreType(this)) {
override fun indexToScore(index: Int): Float =
when (trackPreferences.getScoreType(this)) {
// 10 point
POINT_10 -> index * 10f
// 100 point
@@ -104,7 +102,6 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
POINT_10_DECIMAL -> index.toFloat()
else -> throw Exception("Unknown score type")
}
}
override fun displayScore(track: Track): String {
val score = track.score
@@ -125,9 +122,7 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
}
}
private suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
private suspend fun add(track: Track): Track = api.addLibManga(track)
override suspend fun update(
track: Track,
@@ -190,9 +185,7 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
}
}
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override suspend fun search(query: String): List<TrackSearch> = api.search(query)
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getLibManga(track, getUsername().toInt())
@@ -202,9 +195,7 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
return track
}
override fun authUrl(): String {
return AnilistApi.authUrl().toString()
}
override fun authUrl(): String = AnilistApi.authUrl().toString()
override suspend fun authCallback(url: String) {
val token = url.extractToken("access_token") ?: throw IOException("cannot find token")
@@ -241,12 +232,11 @@ class Anilist(id: Int) : Tracker(id, "AniList"), DeletableTrackService {
trackPreferences.setTrackToken(this, json.encodeToString(oAuth))
}
fun loadOAuth(): OAuth? {
return try {
fun loadOAuth(): OAuth? =
try {
json.decodeFromString<OAuth>(trackPreferences.getTrackToken(this)!!)
} catch (e: Exception) {
logger.error(e) { "loadOAuth err" }
null
}
}
}

View File

@@ -30,17 +30,21 @@ import java.util.Calendar
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
class AnilistApi(
val client: OkHttpClient,
interceptor: AnilistInterceptor,
) {
private val json: Json by injectLazy()
private val authClient =
client.newBuilder()
client
.newBuilder()
.addInterceptor(interceptor)
.rateLimit(permits = 85, period = 1.minutes)
.build()
suspend fun addLibManga(track: Track): Track {
return withIOContext {
suspend fun addLibManga(track: Track): Track =
withIOContext {
val query =
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
@@ -61,25 +65,27 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
with(json) {
authClient.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let {
track.library_id =
it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
it["data"]!!
.jsonObject["SaveMediaListEntry"]!!
.jsonObject["id"]!!
.jsonPrimitive.long
track
}
}
}
}
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
suspend fun updateLibManga(track: Track): Track =
withIOContext {
val query =
"""
|mutation UpdateManga(
@@ -109,14 +115,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("completedAt", createDate(track.finished_reading_date))
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
authClient
.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
track
}
}
suspend fun deleteLibManga(track: Track) {
return withIOContext {
suspend fun deleteLibManga(track: Track) =
withIOContext {
val query =
"""
|mutation DeleteManga(${'$'}listId: Int) {
@@ -133,13 +139,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("listId", track.library_id)
}
}
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
authClient
.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
.awaitSuccess()
}
}
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
suspend fun search(search: String): List<TrackSearch> =
withIOContext {
val query =
"""
|query Search(${'$'}query: String) {
@@ -174,13 +180,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
with(json) {
authClient.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let { response ->
val data = response["data"]!!.jsonObject
@@ -191,13 +197,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
}
}
suspend fun findLibManga(
track: Track,
userid: Int,
): Track? {
return withIOContext {
): Track? =
withIOContext {
val query =
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
@@ -249,13 +254,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
with(json) {
authClient.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let { response ->
val data = response["data"]!!.jsonObject
@@ -266,21 +271,17 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
}
}
suspend fun getLibManga(
track: Track,
userid: Int,
): Track {
return findLibManga(track, userid) ?: throw Exception("Could not find manga")
}
): Track = findLibManga(track, userid) ?: throw Exception("Could not find manga")
fun createOAuth(token: String): OAuth {
return OAuth(token, "Bearer", System.currentTimeMillis() + 365.days.inWholeMilliseconds, 365.days.inWholeMilliseconds)
}
fun createOAuth(token: String): OAuth =
OAuth(token, "Bearer", System.currentTimeMillis() + 365.days.inWholeMilliseconds, 365.days.inWholeMilliseconds)
suspend fun getCurrentUser(): Pair<Int, String> {
return withIOContext {
suspend fun getCurrentUser(): Pair<Int, String> =
withIOContext {
val query =
"""
|query User {
@@ -298,13 +299,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", query)
}
with(json) {
authClient.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
API_URL,
body = payload.toString().toRequestBody(jsonMime),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let {
val data = it["data"]!!.jsonObject
@@ -316,10 +317,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
}
}
}
private fun jsonToALManga(struct: JsonObject): ALManga {
return ALManga(
private fun jsonToALManga(struct: JsonObject): ALManga =
ALManga(
struct["id"]!!.jsonPrimitive.long,
struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content,
struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
@@ -329,10 +329,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0,
)
}
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(
private fun jsonToALUserManga(struct: JsonObject): ALUserManga =
ALUserManga(
struct["id"]!!.jsonPrimitive.long,
struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int,
@@ -341,13 +340,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject),
)
}
private fun parseDate(
struct: JsonObject,
dateKey: String,
): Long {
return try {
): Long =
try {
val date = Calendar.getInstance()
date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
@@ -358,7 +356,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} catch (_: Exception) {
0L
}
}
private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) {
@@ -384,12 +381,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private const val BASE_URL = "https://anilist.co/api/v2/"
private const val BASE_MANGA_URL = "https://anilist.co/manga/"
fun mangaUrl(mediaId: Long): String {
return BASE_MANGA_URL + mediaId
}
fun mangaUrl(mediaId: Long): String = BASE_MANGA_URL + mediaId
fun authUrl(): Uri =
"${BASE_URL}oauth/authorize".toUri().buildUpon()
"${BASE_URL}oauth/authorize"
.toUri()
.buildUpon()
.appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("response_type", "token")
.build()

View File

@@ -5,7 +5,9 @@ import okhttp3.Response
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
import java.io.IOException
class AnilistInterceptor(private val anilist: Anilist) : Interceptor {
class AnilistInterceptor(
private val anilist: Anilist,
) : Interceptor {
/**
* OAuth object used for authenticated requests.
*
@@ -40,7 +42,8 @@ class AnilistInterceptor(private val anilist: Anilist) : Interceptor {
// Add the authorization header to the original request.
val authRequest =
originalRequest.newBuilder()
originalRequest
.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build()

View File

@@ -9,7 +9,10 @@ import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.toTrackSearc
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
class MangaUpdates(id: Int) : Tracker(id, "MangaUpdates"), DeletableTrackService {
class MangaUpdates(
id: Int,
) : Tracker(id, "MangaUpdates"),
DeletableTrackService {
companion object {
const val READING_LIST = 0
const val WISH_LIST = 1
@@ -39,9 +42,7 @@ class MangaUpdates(id: Int) : Tracker(id, "MangaUpdates"), DeletableTrackService
override fun getLogo(): String = "/static/tracker/manga_updates.png"
override fun getStatusList(): List<Int> {
return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
}
override fun getStatusList(): List<Int> = listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST)
override fun getStatus(status: Int): String? =
when (status) {
@@ -83,8 +84,8 @@ class MangaUpdates(id: Int) : Tracker(id, "MangaUpdates"), DeletableTrackService
override suspend fun bind(
track: Track,
hasReadChapters: Boolean,
): Track {
return try {
): Track =
try {
val (series, rating) = api.getSeriesListItem(track)
track.copyFrom(series, rating)
} catch (e: Exception) {
@@ -92,14 +93,13 @@ class MangaUpdates(id: Int) : Tracker(id, "MangaUpdates"), DeletableTrackService
api.addSeriesToList(track, hasReadChapters)
track
}
}
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
override suspend fun search(query: String): List<TrackSearch> =
api
.search(query)
.map {
it.toTrackSearch(id)
}
}
override suspend fun refresh(track: Track): Track {
val (series, rating) = api.getSeriesListItem(track)
@@ -124,7 +124,5 @@ class MangaUpdates(id: Int) : Tracker(id, "MangaUpdates"), DeletableTrackService
interceptor.newAuth(authenticated.sessionToken)
}
fun restoreSession(): String? {
return trackPreferences.getTrackPassword(this)
}
fun restoreSession(): String? = trackPreferences.getTrackPassword(this)
}

View File

@@ -39,7 +39,8 @@ class MangaUpdatesApi(
private val contentType = "application/vnd.api+json".toMediaType()
private val authClient by lazy {
client.newBuilder()
client
.newBuilder()
.addInterceptor(interceptor)
.build()
}
@@ -47,7 +48,8 @@ class MangaUpdatesApi(
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
val listItem =
with(json) {
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
authClient
.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
.awaitSuccess()
.parseAs<ListItem>()
}
@@ -71,13 +73,13 @@ class MangaUpdatesApi(
put("list_id", status)
}
}
authClient.newCall(
POST(
url = "$baseUrl/v1/lists/series",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
url = "$baseUrl/v1/lists/series",
body = body.toString().toRequestBody(contentType),
),
).awaitSuccess()
.let {
if (it.code == 200) {
track.status = status
@@ -99,13 +101,13 @@ class MangaUpdatesApi(
}
}
}
authClient.newCall(
POST(
url = "$baseUrl/v1/lists/series/update",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
url = "$baseUrl/v1/lists/series/update",
body = body.toString().toRequestBody(contentType),
),
).awaitSuccess()
updateSeriesRating(track)
}
@@ -115,26 +117,26 @@ class MangaUpdatesApi(
buildJsonArray {
add(track.media_id)
}
authClient.newCall(
POST(
url = "$baseUrl/v1/lists/series/delete",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
authClient
.newCall(
POST(
url = "$baseUrl/v1/lists/series/delete",
body = body.toString().toRequestBody(contentType),
),
).awaitSuccess()
}
private suspend fun getSeriesRating(track: Track): Rating? {
return try {
private suspend fun getSeriesRating(track: Track): Rating? =
try {
with(json) {
authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
authClient
.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
.awaitSuccess()
.parseAs<Rating>()
}
} catch (e: Exception) {
null
}
}
private suspend fun updateSeriesRating(track: Track) {
if (track.score != 0f) {
@@ -142,20 +144,20 @@ class MangaUpdatesApi(
buildJsonObject {
put("rating", track.score)
}
authClient.newCall(
PUT(
url = "$baseUrl/v1/series/${track.media_id}/rating",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
authClient
.newCall(
PUT(
url = "$baseUrl/v1/series/${track.media_id}/rating",
body = body.toString().toRequestBody(contentType),
),
).awaitSuccess()
} else {
authClient.newCall(
DELETE(
url = "$baseUrl/v1/series/${track.media_id}/rating",
),
)
.awaitSuccess()
authClient
.newCall(
DELETE(
url = "$baseUrl/v1/series/${track.media_id}/rating",
),
).awaitSuccess()
}
}
@@ -172,20 +174,19 @@ class MangaUpdatesApi(
)
}
return with(json) {
client.newCall(
POST(
url = "$baseUrl/v1/series/search",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
client
.newCall(
POST(
url = "$baseUrl/v1/series/search",
body = body.toString().toRequestBody(contentType),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let { obj ->
obj["results"]?.jsonArray?.map { element ->
json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!)
}
}
.orEmpty()
}.orEmpty()
}
}
@@ -199,13 +200,13 @@ class MangaUpdatesApi(
put("password", password)
}
return with(json) {
client.newCall(
PUT(
url = "$baseUrl/v1/account/login",
body = body.toString().toRequestBody(contentType),
),
)
.awaitSuccess()
client
.newCall(
PUT(
url = "$baseUrl/v1/account/login",
body = body.toString().toRequestBody(contentType),
),
).awaitSuccess()
.parseAs<JsonObject>()
.let { obj ->
try {

View File

@@ -17,7 +17,8 @@ class MangaUpdatesInterceptor(
// Add the authorization header to the original request.
val authRequest =
originalRequest.newBuilder()
originalRequest
.newBuilder()
.addHeader("Authorization", "Bearer $token")
.header("User-Agent", "Suwayomi ${BuildConfig.VERSION} (${BuildConfig.REVISION})")
.build()

View File

@@ -14,9 +14,8 @@ data class ListItem(
val priority: Int? = null,
)
fun ListItem.copyTo(track: Track): Track {
return track.apply {
fun ListItem.copyTo(track: Track): Track =
track.apply {
this.status = listId ?: READING_LIST
this.last_chapter_read = this@copyTo.status?.chapter?.toFloat() ?: 0f
}
}

View File

@@ -8,8 +8,7 @@ data class Rating(
val rating: Float? = null,
)
fun Rating.copyTo(track: Track): Track {
return track.apply {
fun Rating.copyTo(track: Track): Track =
track.apply {
this.score = rating ?: 0f
}
}

View File

@@ -23,12 +23,10 @@ data class Record(
val latestChapter: Int? = null,
)
private fun String.htmlDecode(): String {
return Jsoup.parse(this).wholeText()
}
private fun String.htmlDecode(): String = Jsoup.parse(this).wholeText()
fun Record.toTrackSearch(id: Int): TrackSearch {
return TrackSearch.create(id).apply {
fun Record.toTrackSearch(id: Int): TrackSearch =
TrackSearch.create(id).apply {
media_id = this@toTrackSearch.seriesId ?: 0L
title = this@toTrackSearch.title?.htmlDecode() ?: ""
total_chapters = 0
@@ -39,4 +37,3 @@ fun Record.toTrackSearch(id: Int): TrackSearch {
publishing_type = this@toTrackSearch.type.toString()
start_date = this@toTrackSearch.year.toString()
}
}

View File

@@ -12,7 +12,10 @@ import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.io.IOException
class MyAnimeList(id: Int) : Tracker(id, "MyAnimeList"), DeletableTrackService {
class MyAnimeList(
id: Int,
) : Tracker(id, "MyAnimeList"),
DeletableTrackService {
companion object {
const val READING = 1
const val COMPLETED = 2
@@ -36,13 +39,9 @@ class MyAnimeList(id: Int) : Tracker(id, "MyAnimeList"), DeletableTrackService {
private val logger = KotlinLogging.logger {}
override fun getLogo(): String {
return "/static/tracker/mal.png"
}
override fun getLogo(): String = "/static/tracker/mal.png"
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
}
override fun getStatusList(): List<Int> = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
@StringRes
override fun getStatus(status: Int): String? =
@@ -62,17 +61,11 @@ class MyAnimeList(id: Int) : Tracker(id, "MyAnimeList"), DeletableTrackService {
override fun getCompletionStatus(): Int = COMPLETED
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
override fun getScoreList(): List<String> = IntRange(0, 10).map(Int::toString)
override fun displayScore(track: Track): String {
return track.score.toInt().toString()
}
override fun displayScore(track: Track): String = track.score.toInt().toString()
private suspend fun add(track: Track): Track {
return api.updateItem(track)
}
private suspend fun add(track: Track): Track = api.updateItem(track)
override suspend fun update(
track: Track,
@@ -138,13 +131,9 @@ class MyAnimeList(id: Int) : Tracker(id, "MyAnimeList"), DeletableTrackService {
return api.search(query)
}
override suspend fun refresh(track: Track): Track {
return api.findListItem(track) ?: add(track)
}
override suspend fun refresh(track: Track): Track = api.findListItem(track) ?: add(track)
override fun authUrl(): String {
return MyAnimeListApi.authUrl().toString()
}
override fun authUrl(): String = MyAnimeListApi.authUrl().toString()
override suspend fun authCallback(url: String) {
val code = url.extractToken("code") ?: throw IOException("cannot find token")
@@ -180,12 +169,11 @@ class MyAnimeList(id: Int) : Tracker(id, "MyAnimeList"), DeletableTrackService {
trackPreferences.setTrackToken(this, json.encodeToString(oAuth))
}
fun loadOAuth(): OAuth? {
return try {
fun loadOAuth(): OAuth? =
try {
json.decodeFromString<OAuth>(trackPreferences.getTrackToken(this)!!)
} catch (e: Exception) {
logger.error(e) { "loadOAuth err" }
null
}
}
}

View File

@@ -32,79 +32,89 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
class MyAnimeListApi(
private val client: OkHttpClient,
interceptor: MyAnimeListInterceptor,
) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun getAccessToken(authCode: String): OAuth {
return withIOContext {
suspend fun getAccessToken(authCode: String): OAuth =
withIOContext {
val formBody: RequestBody =
FormBody.Builder()
FormBody
.Builder()
.add("client_id", CLIENT_ID)
.add("code", authCode)
.add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code")
.build()
with(json) {
client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
client
.newCall(POST("$BASE_OAUTH_URL/token", body = formBody))
.awaitSuccess()
.parseAs()
}
}
}
suspend fun getCurrentUser(): String {
return withIOContext {
suspend fun getCurrentUser(): String =
withIOContext {
val request =
Request.Builder()
Request
.Builder()
.url("$BASE_API_URL/users/@me")
.get()
.build()
with(json) {
authClient.newCall(request)
authClient
.newCall(request)
.awaitSuccess()
.parseAs<JsonObject>()
.let { it["name"]!!.jsonPrimitive.content }
}
}
}
suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
suspend fun search(query: String): List<TrackSearch> =
withIOContext {
val url =
"$BASE_API_URL/manga".toUri().buildUpon()
"$BASE_API_URL/manga"
.toUri()
.buildUpon()
// MAL API throws a 400 when the query is over 64 characters...
.appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
authClient
.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<JsonObject>()
.let {
it["data"]!!.jsonArray
it["data"]!!
.jsonArray
.map { data -> data.jsonObject["node"]!!.jsonObject }
.map { node ->
val id = node["id"]!!.jsonPrimitive.int
async { getMangaDetails(id) }
}
.awaitAll()
}.awaitAll()
.filter { trackSearch -> !trackSearch.publishing_type.contains("novel") }
}
}
}
}
suspend fun getMangaDetails(id: Int): TrackSearch {
return withIOContext {
suspend fun getMangaDetails(id: Int): TrackSearch =
withIOContext {
val url =
"$BASE_API_URL/manga".toUri().buildUpon()
"$BASE_API_URL/manga"
.toUri()
.buildUpon()
.appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build()
with(json) {
authClient.newCall(GET(url.toString()))
authClient
.newCall(GET(url.toString()))
.awaitSuccess()
.parseAs<JsonObject>()
.let {
@@ -115,7 +125,11 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
cover_url =
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
obj["main_picture"]
?.jsonObject
?.get("large")
?.jsonPrimitive
?.content
?: ""
tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status =
@@ -133,12 +147,12 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
}
}
}
suspend fun updateItem(track: Track): Track {
return withIOContext {
suspend fun updateItem(track: Track): Track =
withIOContext {
val formBodyBuilder =
FormBody.Builder()
FormBody
.Builder()
.add("status", track.toMyAnimeListStatus() ?: "reading")
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
.add("score", track.score.toString())
@@ -151,40 +165,45 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
val request =
Request.Builder()
Request
.Builder()
.url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build())
.build()
with(json) {
authClient.newCall(request)
authClient
.newCall(request)
.awaitSuccess()
.parseAs<JsonObject>()
.let { parseMangaItem(it, track) }
}
}
}
suspend fun deleteItem(track: Track) {
return withIOContext {
suspend fun deleteItem(track: Track) =
withIOContext {
val request =
Request.Builder()
Request
.Builder()
.url(mangaUrl(track.media_id).toString())
.delete()
.build()
authClient.newCall(request)
authClient
.newCall(request)
.awaitSuccess()
}
}
suspend fun findListItem(track: Track): Track? {
return withIOContext {
suspend fun findListItem(track: Track): Track? =
withIOContext {
val uri =
"$BASE_API_URL/manga".toUri().buildUpon()
"$BASE_API_URL/manga"
.toUri()
.buildUpon()
.appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build()
with(json) {
authClient.newCall(GET(uri.toString()))
authClient
.newCall(GET(uri.toString()))
.awaitSuccess()
.parseAs<JsonObject>()
.let { obj ->
@@ -195,43 +214,50 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
}
}
}
suspend fun findListItems(
query: String,
offset: Int = 0,
): List<TrackSearch> {
return withIOContext {
): List<TrackSearch> =
withIOContext {
val json = getListPage(offset)
val obj = json.jsonObject
val matches =
obj["data"]!!.jsonArray
obj["data"]!!
.jsonArray
.filter {
it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains(
query,
ignoreCase = true,
)
}
.map {
val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int
}.map {
val id =
it.jsonObject["node"]!!
.jsonObject["id"]!!
.jsonPrimitive.int
async { getMangaDetails(id) }
}
.awaitAll()
}.awaitAll()
// Check next page if there's more
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
if (!obj["paging"]!!
.jsonObject["next"]
?.jsonPrimitive
?.contentOrNull
.isNullOrBlank()
) {
matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT)
} else {
matches
}
}
}
private suspend fun getListPage(offset: Int): JsonObject {
return withIOContext {
private suspend fun getListPage(offset: Int): JsonObject =
withIOContext {
val urlBuilder =
"$BASE_API_URL/users/@me/mangalist".toUri().buildUpon()
"$BASE_API_URL/users/@me/mangalist"
.toUri()
.buildUpon()
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
.appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString())
if (offset > 0) {
@@ -239,17 +265,18 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
val request =
Request.Builder()
Request
.Builder()
.url(urlBuilder.build().toString())
.get()
.build()
with(json) {
authClient.newCall(request)
authClient
.newCall(request)
.awaitSuccess()
.parseAs()
}
}
}
private fun parseMangaItem(
response: JsonObject,
@@ -270,9 +297,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
}
}
private fun parseDate(isoDate: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
}
private fun parseDate(isoDate: String): Long = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
private fun convertToIsoDate(epochTime: Long): String? {
if (epochTime == 0L) {
@@ -297,21 +322,26 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private var codeVerifier: String = ""
fun authUrl(): Uri =
"$BASE_OAUTH_URL/authorize".toUri().buildUpon()
"$BASE_OAUTH_URL/authorize"
.toUri()
.buildUpon()
.appendQueryParameter("client_id", CLIENT_ID)
.appendQueryParameter("code_challenge", getPkceChallengeCode())
.appendQueryParameter("response_type", "code")
.build()
fun mangaUrl(id: Long): Uri =
"$BASE_API_URL/manga".toUri().buildUpon()
"$BASE_API_URL/manga"
.toUri()
.buildUpon()
.appendPath(id.toString())
.appendPath("my_list_status")
.build()
fun refreshTokenRequest(oauth: OAuth): Request {
val formBody: RequestBody =
FormBody.Builder()
FormBody
.Builder()
.add("client_id", CLIENT_ID)
.add("refresh_token", oauth.refresh_token)
.add("grant_type", "refresh_token")
@@ -321,7 +351,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
// request is called by the interceptor itself so it doesn't reach
// the part where the token is added automatically.
val headers =
Headers.Builder()
Headers
.Builder()
.add("Authorization", "Bearer ${oauth.access_token}")
.build()

View File

@@ -10,7 +10,9 @@ import suwayomi.tachidesk.manga.impl.track.tracker.TokenRefreshFailed
import uy.kohesive.injekt.injectLazy
import java.io.IOException
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
class MyAnimeListInterceptor(
private val myanimelist: MyAnimeList,
) : Interceptor {
private val json: Json by injectLazy()
private var oauth: OAuth? = myanimelist.loadOAuth()
@@ -31,7 +33,8 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
// Add the authorization header to the original request
val authRequest =
originalRequest.newBuilder()
originalRequest
.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()}")
.build()
@@ -72,8 +75,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
response.close()
null
}
}
.getOrNull()
}.getOrNull()
?.also(::setAuth)
?: throw TokenRefreshFailed()
}

View File

@@ -24,7 +24,8 @@ data class UpdateStatus(
) : this(
categories,
mangaStatusMap =
jobs.groupBy { it.status }
jobs
.groupBy { it.status }
.mapValues { entry ->
entry.value.map { it.manga }
}.plus(Pair(JobStatus.SKIPPED, skippedMangas)),

View File

@@ -105,9 +105,7 @@ class Updater : IUpdater {
)
}
override fun getLastUpdateTimestamp(): Long {
return preferences.getLong(lastUpdateKey, 0)
}
override fun getLastUpdateTimestamp(): Long = preferences.getLong(lastUpdateKey, 0)
private fun autoUpdateTask() {
try {
@@ -139,7 +137,10 @@ class Updater : IUpdater {
return
}
val updateInterval = serverConfig.globalUpdateInterval.value.hours.coerceAtLeast(6.hours).inWholeMilliseconds
val updateInterval =
serverConfig.globalUpdateInterval.value.hours
.coerceAtLeast(6.hours)
.inWholeMilliseconds
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval)
@@ -185,26 +186,24 @@ class Updater : IUpdater {
notifyFlow.emit(Unit)
}
private fun getOrCreateUpdateChannelFor(source: String): Channel<UpdateJob> {
return updateChannels.getOrPut(source) {
private fun getOrCreateUpdateChannelFor(source: String): Channel<UpdateJob> =
updateChannels.getOrPut(source) {
logger.debug { "getOrCreateUpdateChannelFor: created channel for $source - channels: ${updateChannels.size + 1}" }
createUpdateChannel(source)
}
}
private fun createUpdateChannel(source: String): Channel<UpdateJob> {
val channel = Channel<UpdateJob>(Channel.UNLIMITED)
channel.consumeAsFlow()
channel
.consumeAsFlow()
.onEach { job ->
semaphore.withPermit {
process(job)
}
}
.catch {
}.catch {
logger.error(it) { "Error during updates (source: $source)" }
handleChannelUpdateFailure(source)
}
.onCompletion { updateChannels.remove(source) }
}.onCompletion { updateChannels.remove(source) }
.launchIn(scope)
return channel
}
@@ -301,22 +300,19 @@ class Updater : IUpdater {
} else {
true
}
}
.filter {
}.filter {
if (it.initialized && serverConfig.excludeNotStarted.value) {
it.lastReadAt != null
} else {
true
}
}
.filter {
}.filter {
if (serverConfig.excludeCompleted.value) {
it.status != MangaStatus.COMPLETED.name
} else {
true
}
}
.filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
}.filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
.toList()
val skippedMangas = categoriesToUpdateMangas.subtract(mangasToUpdate.toSet()).toList()

View File

@@ -58,11 +58,9 @@ object UpdaterSocket : Websocket<UpdateStatus>() {
}
}
fun start(): Job {
return updater.status
fun start(): Job =
updater.status
.onEach {
notifyAllClients(it)
}
.launchIn(scope)
}
}.launchIn(scope)
}

View File

@@ -31,7 +31,9 @@ object BytecodeEditor {
*/
fun fixAndroidClasses(jarFile: Path) {
FileSystems.newFileSystem(jarFile, null as ClassLoader?)?.use {
Files.walk(it.getPath("/")).asSequence()
Files
.walk(it.getPath("/"))
.asSequence()
.filterNotNull()
.filterNot(Files::isDirectory)
.mapNotNull(::getClassBytes)

View File

@@ -48,34 +48,24 @@ private fun getChapterDir(
return getMangaDir(mangaId) + "/$chapterDir"
}
fun getThumbnailDownloadPath(mangaId: Int): String {
return applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
}
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
fun getMangaDownloadDir(mangaId: Int): String {
return applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
}
fun getMangaDownloadDir(mangaId: Int): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
fun getChapterDownloadPath(
mangaId: Int,
chapterId: Int,
): String {
return applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId)
}
): String = applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId)
fun getChapterCbzPath(
mangaId: Int,
chapterId: Int,
): String {
return getChapterDownloadPath(mangaId, chapterId) + ".cbz"
}
): String = getChapterDownloadPath(mangaId, chapterId) + ".cbz"
fun getChapterCachePath(
mangaId: Int,
chapterId: Int,
): String {
return applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
}
): String = applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
/** return value says if rename/move was successful */
fun updateMangaDownloadDir(
@@ -103,6 +93,4 @@ fun updateMangaDownloadDir(
}
}
private fun getMangaEntry(mangaId: Int): ResultRow {
return transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
}
private fun getMangaEntry(mangaId: Int): ResultRow = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }

View File

@@ -75,11 +75,14 @@ fun createComicInfoFile(
val chapterUrl = chapter[ChapterTable.realUrl].orEmpty()
val categories =
transaction {
CategoryMangaTable.innerJoin(CategoryTable).select {
CategoryMangaTable.manga eq manga[MangaTable.id]
}.orderBy(CategoryTable.order to SortOrder.ASC).map {
it[CategoryTable.name]
}
CategoryMangaTable
.innerJoin(CategoryTable)
.select {
CategoryMangaTable.manga eq manga[MangaTable.id]
}.orderBy(CategoryTable.order to SortOrder.ASC)
.map {
it[CategoryTable.name]
}
}.takeUnless { it.isEmpty() }
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
// Remove the old file

View File

@@ -104,7 +104,9 @@ object PackageTools {
Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()
appTag
?.childNodes
?.toList()
.orEmpty()
.asSequence()
.filter {
@@ -126,7 +128,8 @@ object PackageTools {
parsed.apkSingers.flatMap { it.certificateMetas }
// + parsed.apkV2Singers.flatMap { it.certificateMetas }
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
.map { Signature(it.data) }
.toTypedArray()
}
}

View File

@@ -35,7 +35,9 @@ class UnzippingInterceptor : Interceptor {
val contentLength: Long = body.contentLength()
val responseBody = GzipSource(body.source())
val strippedHeaders: Headers = response.headers.newBuilder().build()
response.newBuilder().headers(strippedHeaders)
response
.newBuilder()
.headers(strippedHeaders)
.body(RealResponseBody(body.contentType().toString(), contentLength, responseBody.buffer()))
.build()
} else {

View File

@@ -61,18 +61,15 @@ object GetCatalogueSource {
return sourceCache[sourceId]!!
}
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? {
return try {
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? =
try {
getCatalogueSource(sourceId)
} catch (e: Exception) {
logger.warn(e) { "getCatalogueSource($sourceId) failed" }
null
}
}
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
return getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
}
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource = getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
sourceCache += sourcePair

View File

@@ -15,58 +15,43 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
open class StubSource(override val id: Long) : CatalogueSource {
open class StubSource(
override val id: Long,
) : CatalogueSource {
override val lang: String = "other"
override val supportsLatest: Boolean = false
override val name: String
get() = id.toString()
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
override fun getFilterList(): FilterList {
return FilterList()
}
override fun getFilterList(): FilterList = FilterList()
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.error(getSourceNotInstalledException())
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.error(getSourceNotInstalledException())
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.error(getSourceNotInstalledException())
override fun toString(): String {
return name
}
override fun toString(): String = name
private fun getSourceNotInstalledException(): SourceNotInstalledException {
return SourceNotInstalledException(id)
}
private fun getSourceNotInstalledException(): SourceNotInstalledException = SourceNotInstalledException(id)
inner class SourceNotInstalledException(val id: Long) :
Exception("Source not installed: $id")
inner class SourceNotInstalledException(
val id: Long,
) : Exception("Source not installed: $id")
}

View File

@@ -15,9 +15,7 @@ import java.io.IOException
import java.io.InputStream
object ImageResponse {
private fun pathToInputStream(path: String): InputStream {
return FileInputStream(path).buffered()
}
private fun pathToInputStream(path: String): InputStream = FileInputStream(path).buffered()
/** find file with name when file extension is not known */
fun findFileNameStartingWith(
@@ -115,7 +113,5 @@ object ImageResponse {
}
}
fun clearImages(saveDir: String): Boolean {
return File(saveDir).deleteRecursively()
}
fun clearImages(saveDir: String): Boolean = File(saveDir).deleteRecursively()
}

View File

@@ -32,9 +32,7 @@ object ImageUtil {
return contentType?.startsWith("image/") ?: false
}
fun findImageType(openStream: () -> InputStream): ImageType? {
return openStream().use { findImageType(it) }
}
fun findImageType(openStream: () -> InputStream): ImageType? = openStream().use { findImageType(it) }
fun findImageType(stream: InputStream): ImageType? {
try {
@@ -146,19 +144,19 @@ object ImageUtil {
return false
}
private fun ByteArray.compareWith(magic: ByteArray): Boolean {
return magic.indices.none { this[it] != magic[it] }
}
private fun ByteArray.compareWith(magic: ByteArray): Boolean = magic.indices.none { this[it] != magic[it] }
private fun charByteArrayOf(vararg bytes: Int): ByteArray {
return ByteArray(bytes.size).apply {
private fun charByteArrayOf(vararg bytes: Int): ByteArray =
ByteArray(bytes.size).apply {
for (i in bytes.indices) {
set(i, bytes[i].toByte())
}
}
}
enum class ImageType(val mime: String, val extension: String) {
enum class ImageType(
val mime: String,
val extension: String,
) {
AVIF("image/avif", "avif"),
GIF("image/gif", "gif"),
HEIF("image/heif", "heif"),