parent
68345e636e
commit
c4c9931ae2
@ -186,6 +186,24 @@ object SettingsTrackingScreen : SearchableSettings {
|
|||||||
},
|
},
|
||||||
logout = trackManager.kavita::logout,
|
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)),
|
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.mangaupdates.MangaUpdates
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
|
import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
|
||||||
|
|
||||||
class TrackManager(context: Context) {
|
class TrackManager(context: Context) {
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ class TrackManager(context: Context) {
|
|||||||
const val KOMGA = 6L
|
const val KOMGA = 6L
|
||||||
const val MANGA_UPDATES = 7L
|
const val MANGA_UPDATES = 7L
|
||||||
const val KAVITA = 8L
|
const val KAVITA = 8L
|
||||||
|
const val SUWAYOMI = 9L
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
@ -31,8 +33,9 @@ class TrackManager(context: Context) {
|
|||||||
val komga = Komga(context, KOMGA)
|
val komga = Komga(context, KOMGA)
|
||||||
val mangaUpdates = MangaUpdates(context, MANGA_UPDATES)
|
val mangaUpdates = MangaUpdates(context, MANGA_UPDATES)
|
||||||
val kavita = Kavita(context, KAVITA)
|
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 }
|
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_shikimori" translatable="false">Shikimori</string>
|
||||||
<string name="tracker_manga_updates" translatable="false">MangaUpdates</string>
|
<string name="tracker_manga_updates" translatable="false">MangaUpdates</string>
|
||||||
<string name="tracker_kavita" translatable="false">Kavita</string>
|
<string name="tracker_kavita" translatable="false">Kavita</string>
|
||||||
|
<string name="tracker_suwayomi" translatable="false">Suwayomi</string>
|
||||||
<string name="manga_tracking_tab">Tracking</string>
|
<string name="manga_tracking_tab">Tracking</string>
|
||||||
<plurals name="num_trackers">
|
<plurals name="num_trackers">
|
||||||
<item quantity="one">%d tracker</item>
|
<item quantity="one">%d tracker</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user