Refactor some tracking-related logic

This commit is contained in:
arkon 2023-08-27 10:41:58 -04:00
parent 7644d7c31e
commit 98d6ce2eaf
28 changed files with 104 additions and 151 deletions

View File

@ -1,8 +1,8 @@
package eu.kanade.domain package eu.kanade.domain
import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SetReadStatus
import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
import eu.kanade.domain.download.interactor.DeleteDownload import eu.kanade.domain.download.interactor.DeleteDownload
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionSources
@ -127,7 +127,7 @@ class DomainModule : InjektModule {
addFactory { SetReadStatus(get(), get(), get(), get()) } addFactory { SetReadStatus(get(), get(), get(), get()) }
addFactory { ShouldUpdateDbChapter() } addFactory { ShouldUpdateDbChapter() }
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) } addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get(), get()) } addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { GetHistory(get()) } addFactory { GetHistory(get()) }

View File

@ -11,7 +11,7 @@ import tachiyomi.domain.chapter.model.toChapterUpdate
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
class SyncChaptersWithTrackServiceTwoWay( class SyncChapterProgressWithTrack(
private val updateChapter: UpdateChapter, private val updateChapter: UpdateChapter,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val getChapterByMangaId: GetChapterByMangaId, private val getChapterByMangaId: GetChapterByMangaId,

View File

@ -1,14 +1,13 @@
package eu.kanade.domain.track.interactor package eu.kanade.domain.track.interactor
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack import tachiyomi.domain.track.interactor.InsertTrack
@ -16,28 +15,34 @@ class RefreshTracks(
private val getTracks: GetTracks, private val getTracks: GetTracks,
private val trackManager: TrackManager, private val trackManager: TrackManager,
private val insertTrack: InsertTrack, private val insertTrack: InsertTrack,
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay, private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
) { ) {
suspend fun await(mangaId: Long) { /**
supervisorScope { * Fetches updated tracking data from all logged in trackers.
getTracks.await(mangaId) *
* @return Failed updates.
*/
suspend fun await(mangaId: Long): List<Pair<TrackService?, Throwable>> {
return supervisorScope {
return@supervisorScope getTracks.await(mangaId)
.map { track -> .map { track ->
async { async {
val service = trackManager.getService(track.syncId) val service = trackManager.getService(track.syncId)
if (service != null && service.isLoggedIn) { return@async try {
try { if (service?.isLoggedIn == true) {
val updatedTrack = service.refresh(track.toDbTrack()) val updatedTrack = service.refresh(track.toDbTrack())
insertTrack.await(updatedTrack.toDomainTrack()!!) insertTrack.await(updatedTrack.toDomainTrack()!!)
syncChaptersWithTrackServiceTwoWay.await(mangaId, track, service) syncChapterProgressWithTrack.await(mangaId, track, service)
} catch (e: Throwable) {
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
} }
null
} catch (e: Throwable) {
service to e
} }
} }
} }
.awaitAll() .awaitAll()
.filterNotNull()
} }
} }
} }

View File

@ -127,7 +127,7 @@ private fun ColumnScope.FilterPage(
trackServices.map { service -> trackServices.map { service ->
val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState() val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
TriStateItem( TriStateItem(
label = stringResource(service.nameRes()), label = service.name,
state = filterTracker, state = filterTracker,
onClick = { screenModel.toggleTracker(service.id.toInt()) }, onClick = { screenModel.toggleTracker(service.id.toInt()) },
) )

View File

@ -114,9 +114,7 @@ object SettingsTrackingScreen : SearchableSettings {
if (enhancedTrackers.second.isNotEmpty()) { if (enhancedTrackers.second.isNotEmpty()) {
val missingSourcesInfo = stringResource( val missingSourcesInfo = stringResource(
R.string.enhanced_services_not_installed, R.string.enhanced_services_not_installed,
enhancedTrackers.second enhancedTrackers.second.joinToString { it.name },
.map { stringResource(it.nameRes()) }
.joinToString(),
) )
enhancedTrackerInfo += "\n\n$missingSourcesInfo" enhancedTrackerInfo += "\n\n$missingSourcesInfo"
} }
@ -130,37 +128,37 @@ object SettingsTrackingScreen : SearchableSettings {
title = stringResource(R.string.services), title = stringResource(R.string.services),
preferenceItems = listOf( preferenceItems = listOf(
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(trackManager.myAnimeList.nameRes()), title = trackManager.myAnimeList.name,
service = trackManager.myAnimeList, service = trackManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackManager.myAnimeList) }, logout = { dialog = LogoutDialog(trackManager.myAnimeList) },
), ),
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(trackManager.aniList.nameRes()), title = trackManager.aniList.name,
service = trackManager.aniList, service = trackManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackManager.aniList) }, logout = { dialog = LogoutDialog(trackManager.aniList) },
), ),
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(trackManager.kitsu.nameRes()), title = trackManager.kitsu.name,
service = trackManager.kitsu, service = trackManager.kitsu,
login = { dialog = LoginDialog(trackManager.kitsu, R.string.email) }, login = { dialog = LoginDialog(trackManager.kitsu, R.string.email) },
logout = { dialog = LogoutDialog(trackManager.kitsu) }, logout = { dialog = LogoutDialog(trackManager.kitsu) },
), ),
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(trackManager.mangaUpdates.nameRes()), title = trackManager.mangaUpdates.name,
service = trackManager.mangaUpdates, service = trackManager.mangaUpdates,
login = { dialog = LoginDialog(trackManager.mangaUpdates, R.string.username) }, login = { dialog = LoginDialog(trackManager.mangaUpdates, R.string.username) },
logout = { dialog = LogoutDialog(trackManager.mangaUpdates) }, logout = { dialog = LogoutDialog(trackManager.mangaUpdates) },
), ),
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(trackManager.shikimori.nameRes()), title = trackManager.shikimori.name,
service = trackManager.shikimori, service = trackManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackManager.shikimori) }, logout = { dialog = LogoutDialog(trackManager.shikimori) },
), ),
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(trackManager.bangumi.nameRes()), title = trackManager.bangumi.name,
service = trackManager.bangumi, service = trackManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackManager.bangumi) }, logout = { dialog = LogoutDialog(trackManager.bangumi) },
@ -173,7 +171,7 @@ object SettingsTrackingScreen : SearchableSettings {
preferenceItems = enhancedTrackers.first preferenceItems = enhancedTrackers.first
.map { service -> .map { service ->
Preference.PreferenceItem.TrackingPreference( Preference.PreferenceItem.TrackingPreference(
title = stringResource(service.nameRes()), title = service.name,
service = service, service = service,
login = { (service as EnhancedTrackService).loginNoop() }, login = { (service as EnhancedTrackService).loginNoop() },
logout = service::logout, logout = service::logout,
@ -202,7 +200,7 @@ object SettingsTrackingScreen : SearchableSettings {
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = stringResource(R.string.login_title, stringResource(service.nameRes())), text = stringResource(R.string.login_title, service.name),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
IconButton(onClick = onDismissRequest) { IconButton(onClick = onDismissRequest) {
@ -310,7 +308,7 @@ object SettingsTrackingScreen : SearchableSettings {
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { title = {
Text( Text(
text = stringResource(R.string.logout_title, stringResource(service.nameRes())), text = stringResource(R.string.logout_title, service.name),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )

View File

@ -40,7 +40,7 @@ fun TrackingPreferenceWidget(
) { ) {
TrackLogoIcon(service) TrackLogoIcon(service)
Text( Text(
text = stringResource(service.nameRes()), text = service.name,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),

View File

@ -11,7 +11,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
@ -36,7 +35,7 @@ fun TrackLogoIcon(
) { ) {
Image( Image(
painter = painterResource(service.getLogo()), painter = painterResource(service.getLogo()),
contentDescription = stringResource(service.nameRes()), contentDescription = service.name,
) )
} }
} }

View File

@ -52,7 +52,7 @@ class BackupFileValidator(
val missingTrackers = trackers val missingTrackers = trackers
.mapNotNull { trackManager.getService(it.toLong()) } .mapNotNull { trackManager.getService(it.toLong()) }
.filter { !it.isLoggedIn } .filter { !it.isLoggedIn }
.map { context.getString(it.nameRes()) } .map { it.name }
.sorted() .sorted()
return Results(missingSources, missingTrackers) return Results(missingSources, missingTrackers)

View File

@ -283,7 +283,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
if (libraryPreferences.autoUpdateTrackers().get()) { if (libraryPreferences.autoUpdateTrackers().get()) {
refreshTracks.await(manga.id) refreshTracks(manga.id)
} }
} }
} }
@ -409,13 +409,20 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val manga = libraryManga.manga val manga = libraryManga.manga
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size) notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
refreshTracks.await(manga.id) refreshTracks(manga.id)
} }
notifier.cancelProgressNotification() notifier.cancelProgressNotification()
} }
} }
private suspend fun refreshTracks(mangaId: Long) {
refreshTracks.await(mangaId).forEach { (_, e) ->
// Ignore errors and continue
logcat(LogPriority.ERROR, e)
}
}
private suspend fun withUpdateNotification( private suspend fun withUpdateNotification(
updatingManga: CopyOnWriteArrayList<Manga>, updatingManga: CopyOnWriteArrayList<Manga>,
completed: AtomicInteger, completed: AtomicInteger,

View File

@ -30,7 +30,7 @@ class TrackManager(context: Context) {
val kitsu = Kitsu(KITSU) val kitsu = Kitsu(KITSU)
val shikimori = Shikimori(SHIKIMORI) val shikimori = Shikimori(SHIKIMORI)
val bangumi = Bangumi(BANGUMI) val bangumi = Bangumi(BANGUMI)
val komga = Komga(context, KOMGA) val komga = Komga(KOMGA)
val mangaUpdates = MangaUpdates(MANGA_UPDATES) val mangaUpdates = MangaUpdates(MANGA_UPDATES)
val kavita = Kavita(context, KAVITA) val kavita = Kavita(context, KAVITA)
val suwayomi = Suwayomi(SUWAYOMI) val suwayomi = Suwayomi(SUWAYOMI)

View File

@ -5,7 +5,7 @@ import androidx.annotation.CallSuper
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.domain.track.service.TrackPreferences
@ -28,20 +28,16 @@ import uy.kohesive.injekt.injectLazy
import java.time.ZoneOffset import java.time.ZoneOffset
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
abstract class TrackService(val id: Long) { abstract class TrackService(val id: Long, val name: String) {
val trackPreferences: TrackPreferences by injectLazy() val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy() val networkService: NetworkHelper by injectLazy()
private val insertTrack: InsertTrack by injectLazy() private val insertTrack: InsertTrack by injectLazy()
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay by injectLazy() private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack by injectLazy()
open val client: OkHttpClient open val client: OkHttpClient
get() = networkService.client get() = networkService.client
// Name of the manga sync service to display
@StringRes
abstract fun nameRes(): Int
// Application and remote support for reading dates // Application and remote support for reading dates
open val supportsReadingDates: Boolean = false open val supportsReadingDates: Boolean = false
@ -103,7 +99,7 @@ abstract class TrackService(val id: Long) {
} }
// TODO: move this to an interactor, and update all trackers based on common data // TODO: move this to an interactor, and update all trackers based on common data
suspend fun registerTracking(item: Track, mangaId: Long) { suspend fun register(item: Track, mangaId: Long) {
item.manga_id = mangaId item.manga_id = mangaId
try { try {
withIOContext { withIOContext {
@ -147,7 +143,7 @@ abstract class TrackService(val id: Long) {
} }
} }
syncChaptersWithTrackServiceTwoWay.await(mangaId, track, this@TrackService) syncChapterProgressWithTrack.await(mangaId, track, this@TrackService)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
withUIContext { Injekt.get<Application>().toast(e.message) } withUIContext { Injekt.get<Application>().toast(e.message) }

View File

@ -12,7 +12,7 @@ import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Anilist(id: Long) : TrackService(id), DeletableTrackService { class Anilist(id: Long) : TrackService(id, "AniList"), DeletableTrackService {
companion object { companion object {
const val READING = 1 const val READING = 1
@ -49,9 +49,6 @@ class Anilist(id: Long) : TrackService(id), DeletableTrackService {
} }
} }
@StringRes
override fun nameRes() = R.string.tracker_anilist
override fun getLogo() = R.drawable.ic_tracker_anilist override fun getLogo() = R.drawable.ic_tracker_anilist
override fun getLogoColor() = Color.rgb(18, 25, 35) override fun getLogoColor() = Color.rgb(18, 25, 35)

View File

@ -10,7 +10,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class Bangumi(id: Long) : TrackService(id) { class Bangumi(id: Long) : TrackService(id, "Bangumi") {
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -18,9 +18,6 @@ class Bangumi(id: Long) : TrackService(id) {
private val api by lazy { BangumiApi(client, interceptor) } private val api by lazy { BangumiApi(client, interceptor) }
@StringRes
override fun nameRes() = R.string.tracker_bangumi
override fun getScoreList(): List<String> { override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString) return IntRange(0, 10).map(Int::toString)
} }

View File

@ -14,7 +14,7 @@ import tachiyomi.domain.manga.model.Manga
import java.security.MessageDigest import java.security.MessageDigest
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Kavita(private val context: Context, id: Long) : TrackService(id), EnhancedTrackService { class Kavita(private val context: Context, id: Long) : TrackService(id, "Kavita"), EnhancedTrackService {
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1
@ -27,9 +27,6 @@ class Kavita(private val context: Context, id: Long) : TrackService(id), Enhance
private val interceptor by lazy { KavitaInterceptor(this) } private val interceptor by lazy { KavitaInterceptor(this) }
val api by lazy { KavitaApi(client, interceptor) } val api by lazy { KavitaApi(client, interceptor) }
@StringRes
override fun nameRes() = R.string.tracker_kavita
override fun getLogo(): Int = R.drawable.ic_tracker_kavita override fun getLogo(): Int = R.drawable.ic_tracker_kavita
override fun getLogoColor() = Color.rgb(74, 198, 148) override fun getLogoColor() = Color.rgb(74, 198, 148)

View File

@ -115,8 +115,8 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
} }
private fun getLatestChapterRead(url: String): Float { private fun getLatestChapterRead(url: String): Float {
val serieId = getIdFromUrl(url) val seriesId = getIdFromUrl(url)
val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId" val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$seriesId"
try { try {
with(json) { with(json) {
authClient.newCall(GET(requestUrl)).execute().use { authClient.newCall(GET(requestUrl)).execute().use {
@ -137,21 +137,21 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
try { try {
val serieDto: SeriesDto = with(json) { val seriesDto: SeriesDto = with(json) {
authClient.newCall(GET(url)) authClient.newCall(GET(url))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
val track = serieDto.toTrack() val track = seriesDto.toTrack()
track.apply { track.apply {
cover_url = serieDto.thumbnail_url.toString() cover_url = seriesDto.thumbnail_url.toString()
tracking_url = url tracking_url = url
total_chapters = getTotalChapters(url) total_chapters = getTotalChapters(url)
title = serieDto.name title = seriesDto.name
status = when (serieDto.pagesRead) { status = when (seriesDto.pagesRead) {
serieDto.pages -> Kavita.COMPLETED seriesDto.pages -> Kavita.COMPLETED
0 -> Kavita.UNREAD 0 -> Kavita.UNREAD
else -> Kavita.READING else -> Kavita.READING
} }

View File

@ -12,7 +12,7 @@ import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
class Kitsu(id: Long) : TrackService(id), DeletableTrackService { class Kitsu(id: Long) : TrackService(id, "Kitsu"), DeletableTrackService {
companion object { companion object {
const val READING = 1 const val READING = 1
@ -22,9 +22,6 @@ class Kitsu(id: Long) : TrackService(id), DeletableTrackService {
const val PLAN_TO_READ = 5 const val PLAN_TO_READ = 5
} }
@StringRes
override fun nameRes() = R.string.tracker_kitsu
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
private val json: Json by injectLazy() private val json: Json by injectLazy()

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.komga package eu.kanade.tachiyomi.data.track.komga
import android.content.Context
import android.graphics.Color import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -14,7 +13,7 @@ import okhttp3.OkHttpClient
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Komga(private val context: Context, id: Long) : TrackService(id), EnhancedTrackService { class Komga(id: Long) : TrackService(id, "Komga"), EnhancedTrackService {
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1
@ -29,9 +28,6 @@ class Komga(private val context: Context, id: Long) : TrackService(id), Enhanced
val api by lazy { KomgaApi(client) } val api by lazy { KomgaApi(client) }
@StringRes
override fun nameRes() = R.string.tracker_komga
override fun getLogo() = R.drawable.ic_tracker_komga override fun getLogo() = R.drawable.ic_tracker_komga
override fun getLogoColor() = Color.rgb(51, 37, 50) override fun getLogoColor() = Color.rgb(51, 37, 50)

View File

@ -17,7 +17,7 @@ import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
const val READLIST_API = "/api/v1/readlists" private const val READLIST_API = "/api/v1/readlists"
class KomgaApi(private val client: OkHttpClient) { class KomgaApi(private val client: OkHttpClient) {

View File

@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo
import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
class MangaUpdates(id: Long) : TrackService(id), DeletableTrackService { class MangaUpdates(id: Long) : TrackService(id, "MangaUpdates"), DeletableTrackService {
companion object { companion object {
const val READING_LIST = 0 const val READING_LIST = 0
@ -24,9 +24,6 @@ class MangaUpdates(id: Long) : TrackService(id), DeletableTrackService {
private val api by lazy { MangaUpdatesApi(interceptor, client) } private val api by lazy { MangaUpdatesApi(interceptor, client) }
@StringRes
override fun nameRes(): Int = R.string.tracker_manga_updates
override fun getLogo(): Int = R.drawable.ic_manga_updates override fun getLogo(): Int = R.drawable.ic_manga_updates
override fun getLogoColor(): Int = Color.rgb(146, 160, 173) override fun getLogoColor(): Int = Color.rgb(146, 160, 173)

View File

@ -11,7 +11,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class MyAnimeList(id: Long) : TrackService(id), DeletableTrackService { class MyAnimeList(id: Long) : TrackService(id, "MyAnimeList"), DeletableTrackService {
companion object { companion object {
const val READING = 1 const val READING = 1
@ -30,9 +30,6 @@ class MyAnimeList(id: Long) : TrackService(id), DeletableTrackService {
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) } private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) }
private val api by lazy { MyAnimeListApi(client, interceptor) } private val api by lazy { MyAnimeListApi(client, interceptor) }
@StringRes
override fun nameRes() = R.string.tracker_myanimelist
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
override fun getLogo() = R.drawable.ic_tracker_mal override fun getLogo() = R.drawable.ic_tracker_mal

View File

@ -11,7 +11,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class Shikimori(id: Long) : TrackService(id), DeletableTrackService { class Shikimori(id: Long) : TrackService(id, "Shikimori"), DeletableTrackService {
companion object { companion object {
const val READING = 1 const val READING = 1
@ -28,9 +28,6 @@ class Shikimori(id: Long) : TrackService(id), DeletableTrackService {
private val api by lazy { ShikimoriApi(client, interceptor) } private val api by lazy { ShikimoriApi(client, interceptor) }
@StringRes
override fun nameRes() = R.string.tracker_shikimori
override fun getScoreList(): List<String> { override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString) return IntRange(0, 10).map(Int::toString)
} }

View File

@ -122,7 +122,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
} }
suspend fun findLibManga(track: Track, user_id: String): Track? { suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext { return withIOContext {
val urlMangas = "$apiUrl/mangas".toUri().buildUpon() val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
@ -134,7 +134,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
val url = "$apiUrl/v2/user_rates".toUri().buildUpon() val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", userId)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
.build() .build()

View File

@ -11,13 +11,10 @@ import eu.kanade.tachiyomi.source.Source
import tachiyomi.domain.manga.model.Manga as DomainManga import tachiyomi.domain.manga.model.Manga as DomainManga
import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.domain.track.model.Track as DomainTrack
class Suwayomi(id: Long) : TrackService(id), EnhancedTrackService { class Suwayomi(id: Long) : TrackService(id, "Suwayomi"), EnhancedTrackService {
val api by lazy { TachideskApi() } val api by lazy { TachideskApi() }
@StringRes
override fun nameRes() = R.string.tracker_suwayomi
override fun getLogo() = R.drawable.ic_tracker_suwayomi override fun getLogo() = R.drawable.ic_tracker_suwayomi
override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO override fun getLogoColor() = Color.rgb(255, 35, 35) // TODO

View File

@ -28,19 +28,19 @@ class TachideskApi {
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
private val json: Json by injectLazy() private val json: Json by injectLazy()
val client: OkHttpClient = private val client: OkHttpClient =
network.client.newBuilder() network.client.newBuilder()
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build() .build()
fun headersBuilder(): Headers.Builder = Headers.Builder().apply { private fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) { if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
val credentials = Credentials.basic(baseLogin, basePassword) val credentials = Credentials.basic(baseLogin, basePassword)
add("Authorization", credentials) add("Authorization", credentials)
} }
} }
val headers: Headers by lazy { headersBuilder().build() } private val headers: Headers by lazy { headersBuilder().build() }
private val baseUrl by lazy { getPrefBaseUrl() } private val baseUrl by lazy { getPrefBaseUrl() }
private val baseLogin by lazy { getPrefBaseLogin() } private val baseLogin by lazy { getPrefBaseLogin() }
@ -100,7 +100,7 @@ class TachideskApi {
return getTrackSearch(track.tracking_url) return getTrackSearch(track.tracking_url)
} }
val tachideskExtensionId by lazy { private val tachideskExtensionId by lazy {
val key = "tachidesk/en/1" val key = "tachidesk/en/1"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) 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 (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
@ -110,6 +110,10 @@ class TachideskApi {
Injekt.get<Application>().getSharedPreferences("source_$tachideskExtensionId", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$tachideskExtensionId", 0x0000)
} }
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)!!
companion object { companion object {
private const val ADDRESS_TITLE = "Server URL Address" private const val ADDRESS_TITLE = "Server URL Address"
private const val ADDRESS_DEFAULT = "" private const val ADDRESS_DEFAULT = ""
@ -118,8 +122,4 @@ class TachideskApi {
private const val PASSWORD_TITLE = "Password (Basic Auth)" private const val PASSWORD_TITLE = "Password (Basic Auth)"
private const val PASSWORD_DEFAULT = "" 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)!!
} }

View File

@ -14,7 +14,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.preference.asState import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.chapter.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
@ -77,7 +77,7 @@ class BrowseSourceScreenModel(
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), private val updateManga: UpdateManga = Injekt.get(),
private val insertTrack: InsertTrack = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(),
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack = Injekt.get(),
) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) { ) : StateScreenModel<BrowseSourceScreenModel.State>(State(Listing.valueOf(listingQuery))) {
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLoggedIn } } private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLoggedIn } }
@ -297,7 +297,7 @@ class BrowseSourceScreenModel(
(service as TrackService).bind(track) (service as TrackService).bind(track)
insertTrack.await(track.toDomainTrack()!!) insertTrack.await(track.toDomainTrack()!!)
syncChaptersWithTrackServiceTwoWay.await(manga.id, track.toDomainTrack()!!, service) syncChapterProgressWithTrack.await(manga.id, track.toDomainTrack()!!, service)
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" } logcat(LogPriority.WARN, e) { "Could not match manga: ${manga.title} with service $service" }

View File

@ -323,7 +323,7 @@ class MangaScreenModel(
launchIO { launchIO {
try { try {
service.match(manga)?.let { track -> service.match(manga)?.let { track ->
(service as TrackService).registerTracking(track, mangaId) (service as TrackService).register(track, mangaId)
} }
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.WARN, e) { logcat(LogPriority.WARN, e) {

View File

@ -39,9 +39,8 @@ import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.model.toDbTrack import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.track.TrackChapterSelector import eu.kanade.presentation.track.TrackChapterSelector
import eu.kanade.presentation.track.TrackDateSelector import eu.kanade.presentation.track.TrackDateSelector
@ -74,7 +73,6 @@ import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.DeleteTrack import tachiyomi.domain.track.interactor.DeleteTrack
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
import tachiyomi.domain.track.interactor.InsertTrack
import tachiyomi.domain.track.model.Track import tachiyomi.domain.track.model.Track
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
@ -208,7 +206,7 @@ data class TrackInfoDialogHomeScreen(
val manga = Injekt.get<GetManga>().await(mangaId) ?: return@launchNonCancellable val manga = Injekt.get<GetManga>().await(mangaId) ?: return@launchNonCancellable
try { try {
val matchResult = item.service.match(manga) ?: throw Exception() val matchResult = item.service.match(manga) ?: throw Exception()
item.service.registerTracking(matchResult, mangaId) item.service.register(matchResult, mangaId)
} catch (e: Exception) { } catch (e: Exception) {
withUIContext { Injekt.get<Application>().toast(R.string.error_no_match) } withUIContext { Injekt.get<Application>().toast(R.string.error_no_match) }
} }
@ -216,39 +214,26 @@ data class TrackInfoDialogHomeScreen(
} }
private suspend fun refreshTrackers() { private suspend fun refreshTrackers() {
val insertTrack = Injekt.get<InsertTrack>() val refreshTracks = Injekt.get<RefreshTracks>()
val syncChaptersWithTrackServiceTwoWay = Injekt.get<SyncChaptersWithTrackServiceTwoWay>()
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
try { refreshTracks.await(mangaId)
val trackItems = getTracks.await(mangaId).mapToTrackItem() .filter { it.first != null }
for (trackItem in trackItems) { .forEach { (track, e) ->
try { logcat(LogPriority.ERROR, e) {
val track = trackItem.track ?: continue "Failed to refresh track data mangaId=$mangaId for service ${track!!.id}"
val domainTrack = trackItem.service.refresh(track.toDbTrack()).toDomainTrack() ?: continue }
insertTrack.await(domainTrack)
syncChaptersWithTrackServiceTwoWay.await(mangaId, domainTrack, trackItem.service)
} catch (e: Exception) {
logcat(
LogPriority.ERROR,
e,
) { "Failed to refresh track data mangaId=$mangaId for service ${trackItem.service.id}" }
withUIContext { withUIContext {
context.toast( context.toast(
context.getString( context.getString(
R.string.track_error, R.string.track_error,
context.getString(trackItem.service.nameRes()), track!!.name,
e.message, e.message,
), ),
) )
} }
} }
} }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to refresh track data mangaId=$mangaId" }
withUIContext { context.toast(e.message) }
}
}
private fun List<Track>.mapToTrackItem(): List<TrackItem> { private fun List<Track>.mapToTrackItem(): List<TrackItem> {
val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLoggedIn } val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLoggedIn }
@ -581,7 +566,7 @@ private data class TrackDateRemoverScreen(
) )
}, },
text = { text = {
val serviceName = stringResource(sm.getServiceNameRes()) val serviceName = sm.getServiceName()
Text( Text(
text = if (start) { text = if (start) {
stringResource(R.string.track_remove_start_date_conf_text, serviceName) stringResource(R.string.track_remove_start_date_conf_text, serviceName)
@ -618,7 +603,7 @@ private data class TrackDateRemoverScreen(
private val start: Boolean, private val start: Boolean,
) : ScreenModel { ) : ScreenModel {
fun getServiceNameRes() = service.nameRes() fun getServiceName() = service.name
fun removeDate() { fun removeDate() {
coroutineScope.launchNonCancellable { coroutineScope.launchNonCancellable {
@ -703,7 +688,7 @@ data class TrackServiceSearchScreen(
} }
fun registerTracking(item: TrackSearch) { fun registerTracking(item: TrackSearch) {
coroutineScope.launchNonCancellable { service.registerTracking(item, mangaId) } coroutineScope.launchNonCancellable { service.register(item, mangaId) }
} }
fun updateSelection(selected: TrackSearch) { fun updateSelection(selected: TrackSearch) {
@ -734,7 +719,7 @@ private data class TrackServiceRemoveScreen(
service = Injekt.get<TrackManager>().getService(serviceId)!!, service = Injekt.get<TrackManager>().getService(serviceId)!!,
) )
} }
val serviceName = stringResource(sm.getServiceNameRes()) val serviceName = sm.getServiceName()
var removeRemoteTrack by remember { mutableStateOf(false) } var removeRemoteTrack by remember { mutableStateOf(false) }
AlertDialogContent( AlertDialogContent(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars), modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars),
@ -799,7 +784,7 @@ private data class TrackServiceRemoveScreen(
private val deleteTrack: DeleteTrack = Injekt.get(), private val deleteTrack: DeleteTrack = Injekt.get(),
) : ScreenModel { ) : ScreenModel {
fun getServiceNameRes() = service.nameRes() fun getServiceName() = service.name
fun isServiceDeletable() = service is DeletableTrackService fun isServiceDeletable() = service is DeletableTrackService

View File

@ -697,15 +697,6 @@
<string name="are_you_sure">Are you sure?</string> <string name="are_you_sure">Are you sure?</string>
<!-- Tracking Screen --> <!-- Tracking Screen -->
<string name="tracker_anilist" translatable="false">AniList</string>
<string name="tracker_myanimelist" translatable="false">MyAnimeList</string>
<string name="tracker_kitsu" translatable="false">Kitsu</string>
<string name="tracker_komga" translatable="false">Komga</string>
<string name="tracker_bangumi" translatable="false">Bangumi</string>
<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> <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>