mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-15 00:34:12 -05:00
[skip ci] Formatting
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package suwayomi.tachidesk.manga.impl.download.fileProvider
|
||||
|
||||
interface DownloadedFilesProvider : FileDownloader, FileRetriever {
|
||||
interface DownloadedFilesProvider :
|
||||
FileDownloader,
|
||||
FileRetriever {
|
||||
fun delete(): Boolean
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user