parent
68345e636e
commit
c4c9931ae2
@ -186,6 +186,24 @@ object SettingsTrackingScreen : SearchableSettings {
|
||||
},
|
||||
logout = trackManager.kavita::logout,
|
||||
),
|
||||
|
||||
Preference.PreferenceItem.TrackingPreference(
|
||||
title = stringResource(trackManager.suwayomi.nameRes()),
|
||||
service = trackManager.suwayomi,
|
||||
login = {
|
||||
val sourceManager = Injekt.get<SourceManager>()
|
||||
val acceptedSources = trackManager.suwayomi.getAcceptedSources()
|
||||
val hasValidSourceInstalled = sourceManager.getCatalogueSources()
|
||||
.any { it::class.qualifiedName in acceptedSources }
|
||||
|
||||
if (hasValidSourceInstalled) {
|
||||
trackManager.suwayomi.loginNoop()
|
||||
} else {
|
||||
context.toast(context.getString(R.string.enhanced_tracking_warning, context.getString(trackManager.suwayomi.nameRes())), Toast.LENGTH_LONG)
|
||||
}
|
||||
},
|
||||
logout = trackManager.suwayomi::logout,
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(R.string.enhanced_tracking_info)),
|
||||
),
|
||||
),
|
||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
|
||||
|
||||
class TrackManager(context: Context) {
|
||||
|
||||
@ -21,6 +22,7 @@ class TrackManager(context: Context) {
|
||||
const val KOMGA = 6L
|
||||
const val MANGA_UPDATES = 7L
|
||||
const val KAVITA = 8L
|
||||
const val SUWAYOMI = 9L
|
||||
}
|
||||
|
||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||
@ -31,8 +33,9 @@ class TrackManager(context: Context) {
|
||||
val komga = Komga(context, KOMGA)
|
||||
val mangaUpdates = MangaUpdates(context, MANGA_UPDATES)
|
||||
val kavita = Kavita(context, KAVITA)
|
||||
val suwayomi = Suwayomi(context, SUWAYOMI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita)
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
|
||||
|
||||
fun getService(id: Long) = services.find { it.id == id }
|
||||
|
||||
|
@ -0,0 +1,102 @@
|
||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.domain.manga.model.Manga as DomainManga
|
||||
import eu.kanade.domain.track.model.Track as DomainTrack
|
||||
|
||||
class Suwayomi(private val context: Context, id: Long) : TrackService(id), NoLoginTrackService, EnhancedTrackService {
|
||||
val api by lazy { TachideskApi() }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_suwayomi
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_suwayomi
|
||||
|
||||
override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO
|
||||
|
||||
companion object {
|
||||
const val UNREAD = 1
|
||||
const val READING = 2
|
||||
const val COMPLETED = 3
|
||||
}
|
||||
|
||||
override fun getStatusList() = listOf(UNREAD, READING, COMPLETED)
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
when (status) {
|
||||
UNREAD -> getString(R.string.unread)
|
||||
READING -> getString(R.string.reading)
|
||||
COMPLETED -> getString(R.string.completed)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getReadingStatus(): Int = READING
|
||||
|
||||
override fun getRereadingStatus(): Int = -1
|
||||
|
||||
override fun getCompletionStatus(): Int = COMPLETED
|
||||
|
||||
override fun getScoreList(): List<String> = emptyList()
|
||||
|
||||
override fun displayScore(track: Track): String = ""
|
||||
|
||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||
if (track.status != COMPLETED) {
|
||||
if (didReadChapter) {
|
||||
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||
track.status = COMPLETED
|
||||
} else {
|
||||
track.status = READING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return api.updateProgress(track)
|
||||
}
|
||||
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getTrackSearch(track.tracking_url)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String) {
|
||||
saveCredentials("user", "pass")
|
||||
}
|
||||
|
||||
override fun loginNoop() {
|
||||
saveCredentials("user", "pass")
|
||||
}
|
||||
|
||||
override fun getAcceptedSources(): List<String> = listOf("eu.kanade.tachiyomi.extension.all.tachidesk.Tachidesk")
|
||||
|
||||
override suspend fun match(manga: DomainManga): TrackSearch = api.getTrackSearch(manga.url)
|
||||
|
||||
override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = source?.let { accept(it) } == true
|
||||
|
||||
override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
|
||||
if (accept(newSource)) {
|
||||
track.copy(remoteUrl = manga.url)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Dns
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.nio.charset.Charset
|
||||
import java.security.MessageDigest
|
||||
|
||||
class TachideskApi {
|
||||
private val network by injectLazy<NetworkHelper>()
|
||||
val client: OkHttpClient =
|
||||
network.client.newBuilder()
|
||||
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
|
||||
.build()
|
||||
fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
|
||||
add("User-Agent", network.defaultUserAgent)
|
||||
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
|
||||
val credentials = Credentials.basic(baseLogin, basePassword)
|
||||
add("Authorization", credentials)
|
||||
}
|
||||
}
|
||||
|
||||
val headers: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
private val baseUrl by lazy { getPrefBaseUrl() }
|
||||
private val baseLogin by lazy { getPrefBaseLogin() }
|
||||
private val basePassword by lazy { getPrefBasePassword() }
|
||||
|
||||
suspend fun getTrackSearch(trackUrl: String): TrackSearch = withIOContext {
|
||||
val url = try {
|
||||
// test if getting api url or manga id
|
||||
val mangaId = trackUrl.toLong()
|
||||
"$baseUrl/api/v1/manga/$mangaId"
|
||||
} catch (e: NumberFormatException) {
|
||||
trackUrl
|
||||
}
|
||||
|
||||
val manga = client.newCall(GET("$url/full", headers)).await().parseAs<MangaDataClass>()
|
||||
|
||||
TrackSearch.create(TrackManager.SUWAYOMI).apply {
|
||||
title = manga.title
|
||||
cover_url = "$url/thumbnail"
|
||||
summary = manga.description
|
||||
tracking_url = url
|
||||
total_chapters = manga.chapterCount.toInt()
|
||||
publishing_status = manga.status
|
||||
last_chapter_read = manga.lastChapterRead?.chapterNumber ?: 0F
|
||||
status = when (manga.unreadCount) {
|
||||
manga.chapterCount -> Suwayomi.UNREAD
|
||||
0L -> Suwayomi.COMPLETED
|
||||
else -> Suwayomi.READING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProgress(track: Track): Track {
|
||||
val url = track.tracking_url
|
||||
val chapters = client.newCall(GET("$url/chapters", headers)).await().parseAs<List<ChapterDataClass>>()
|
||||
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
|
||||
|
||||
client.newCall(
|
||||
PUT(
|
||||
"$url/chapter/$lastChapterIndex",
|
||||
headers,
|
||||
FormBody.Builder(Charset.forName("utf8"))
|
||||
.add("markPrevRead", "true")
|
||||
.add("read", "true")
|
||||
.build(),
|
||||
),
|
||||
).await()
|
||||
|
||||
return getTrackSearch(track.tracking_url)
|
||||
}
|
||||
|
||||
val tachideskExtensionId by lazy {
|
||||
val key = "tachidesk/en/1"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$tachideskExtensionId", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ADDRESS_TITLE = "Server URL Address"
|
||||
private const val ADDRESS_DEFAULT = ""
|
||||
private const val LOGIN_TITLE = "Login (Basic Auth)"
|
||||
private const val LOGIN_DEFAULT = ""
|
||||
private const val PASSWORD_TITLE = "Password (Basic Auth)"
|
||||
private const val PASSWORD_DEFAULT = ""
|
||||
}
|
||||
|
||||
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
|
||||
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
|
||||
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package eu.kanade.tachiyomi.data.track.suwayomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SourceDataClass(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val lang: String?,
|
||||
val iconUrl: String?,
|
||||
|
||||
/** The Source provides a latest listing */
|
||||
val supportsLatest: Boolean?,
|
||||
|
||||
/** The Source implements [ConfigurableSource] */
|
||||
val isConfigurable: Boolean?,
|
||||
|
||||
/** The Source class has a @Nsfw annotation */
|
||||
val isNsfw: Boolean?,
|
||||
|
||||
/** A nicer version of [name] */
|
||||
val displayName: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaDataClass(
|
||||
val id: Int,
|
||||
val sourceId: String,
|
||||
|
||||
val url: String,
|
||||
val title: String,
|
||||
val thumbnailUrl: String,
|
||||
|
||||
val initialized: Boolean,
|
||||
|
||||
val artist: String,
|
||||
val author: String,
|
||||
val description: String,
|
||||
val genre: List<String>,
|
||||
val status: String,
|
||||
val inLibrary: Boolean,
|
||||
val inLibraryAt: Long,
|
||||
val source: SourceDataClass,
|
||||
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
|
||||
val realUrl: String,
|
||||
var lastFetchedAt: Long,
|
||||
var chaptersLastFetchedAt: Long,
|
||||
|
||||
val freshData: Boolean,
|
||||
val unreadCount: Long,
|
||||
val downloadCount: Long,
|
||||
val chapterCount: Long,
|
||||
val lastChapterRead: ChapterDataClass?,
|
||||
|
||||
val age: Long,
|
||||
val chaptersAge: Long,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterDataClass(
|
||||
val id: Int,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val uploadDate: Long,
|
||||
val chapterNumber: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
|
||||
/** chapter is read */
|
||||
val read: Boolean,
|
||||
|
||||
/** chapter is bookmarked */
|
||||
val bookmarked: Boolean,
|
||||
|
||||
/** last read page, zero means not read/no data */
|
||||
val lastPageRead: Int,
|
||||
|
||||
/** last read page, zero means not read/no data */
|
||||
val lastReadAt: Long,
|
||||
|
||||
/** this chapter's index, starts with 1 */
|
||||
val index: Int,
|
||||
|
||||
/** the date we fist saw this chapter*/
|
||||
val fetchedAt: Long,
|
||||
|
||||
/** is chapter downloaded */
|
||||
val downloaded: Boolean,
|
||||
|
||||
/** used to construct pages in the front-end */
|
||||
val pageCount: Int,
|
||||
|
||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
||||
val chapterCount: Int,
|
||||
)
|
BIN
app/src/main/res/drawable-nodpi/ic_tracker_suwayomi.webp
Normal file
BIN
app/src/main/res/drawable-nodpi/ic_tracker_suwayomi.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -682,6 +682,7 @@
|
||||
<string name="tracker_shikimori" translatable="false">Shikimori</string>
|
||||
<string name="tracker_manga_updates" translatable="false">MangaUpdates</string>
|
||||
<string name="tracker_kavita" translatable="false">Kavita</string>
|
||||
<string name="tracker_suwayomi" translatable="false">Suwayomi</string>
|
||||
<string name="manga_tracking_tab">Tracking</string>
|
||||
<plurals name="num_trackers">
|
||||
<item quantity="one">%d tracker</item>
|
||||
|
Loading…
Reference in New Issue
Block a user