Update Mangadex Similar to GoldBattles latest version

This commit is contained in:
Jobobby04 2021-05-12 23:29:14 -04:00
parent e500d0bebf
commit 08f1eff450
25 changed files with 70 additions and 853 deletions

View File

@ -189,10 +189,6 @@
android:exported="false" />
<!-- EH -->
<service
android:name="exh.md.similar.SimilarUpdateService"
android:exported="false" />
<service
android:name="exh.eh.EHentaiUpdateWorker"
android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -21,9 +21,6 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
import exh.md.similar.sql.mappers.SimilarTypeMapping
import exh.md.similar.sql.models.MangaSimilar
import exh.md.similar.sql.queries.SimilarQueries
import exh.merged.sql.mappers.MergedMangaTypeMapping
import exh.merged.sql.models.MergedMangaReference
import exh.merged.sql.queries.MergedQueries
@ -42,7 +39,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME)
@ -62,7 +59,6 @@ open class DatabaseHelper(context: Context) :
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
.addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
// SY <--
.build()

View File

@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable
import exh.md.similar.sql.tables.SimilarTable
import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable
@ -25,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = /* SY --> */ 6 /* SY <-- */
const val DATABASE_VERSION = /* SY --> */ 7 /* SY <-- */
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -40,7 +39,6 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTagTable.createTableQuery)
execSQL(SearchTitleTable.createTableQuery)
execSQL(MergedTable.createTableQuery)
execSQL(SimilarTable.createTableQuery)
// SY <--
// DB indexes
@ -57,7 +55,6 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTitleTable.createMangaIdIndexQuery)
execSQL(SearchTitleTable.createTitleIndexQuery)
execSQL(MergedTable.createIndexQuery)
execSQL(SimilarTable.createMangaIdIndexQuery)
// SY <--
}
@ -74,13 +71,16 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(MergedTable.createTableQuery)
db.execSQL(MergedTable.createIndexQuery)
}
if (oldVersion < 5) {
/*if (oldVersion < 5) {
db.execSQL(SimilarTable.createTableQuery)
db.execSQL(SimilarTable.createMangaIdIndexQuery)
}
}*/
if (oldVersion < 6) {
db.execSQL(MangaTable.addFilteredScanlators)
}
if (oldVersion < 7) {
db.execSQL("DROP TABLE IF EXISTS manga_related")
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -26,7 +26,6 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import exh.md.similar.SimilarUpdateService
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -111,9 +110,6 @@ class NotificationReceiver : BroadcastReceiver() {
"text/plain",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// SY -->
ACTION_CANCEL_SIMILAR_UPDATE -> cancelSimilarUpdate(context)
// SY <--
}
}
@ -255,18 +251,6 @@ class NotificationReceiver : BroadcastReceiver() {
}
}
// SY -->
/**
* Method called when user wants to stop a similar manga update
*
* @param context context of application
*/
private fun cancelSimilarUpdate(context: Context) {
SimilarUpdateService.stop(context)
Handler().post { dismissNotification(context, Notifications.ID_SIMILAR_PROGRESS) }
}
// SY <--
companion object {
private const val NAME = "NotificationReceiver"
@ -299,11 +283,6 @@ class NotificationReceiver : BroadcastReceiver() {
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
// Sy -->
// Called to cancel similar manga update.
private const val ACTION_CANCEL_SIMILAR_UPDATE = "$ID.$NAME.CANCEL_SIMILAR_UPDATE"
// SY <--
/**
* Returns a [PendingIntent] that resumes the download of a chapter
*
@ -572,20 +551,5 @@ class NotificationReceiver : BroadcastReceiver() {
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
// SY -->
/**
* Returns [PendingIntent] that starts a service which stops the similar update
*
* @param context context of application
* @return [PendingIntent]
*/
internal fun cancelSimilarUpdatePendingBroadcast(context: Context): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_CANCEL_SIMILAR_UPDATE
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
// SY <--
}
}

View File

@ -74,15 +74,6 @@ object Notifications {
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
const val ID_INCOGNITO_MODE = -701
// SY -->
/**
* Notification channel and ids used for backup and restore.
*/
const val CHANNEL_SIMILAR = "similar_channel"
const val ID_SIMILAR_PROGRESS = -901
const val ID_SIMILAR_COMPLETE = -902
// SY <--
private val deprecatedChannels = listOf(
"downloader_channel",
"backup_restore_complete_channel"
@ -174,14 +165,7 @@ object Notifications {
CHANNEL_INCOGNITO_MODE,
context.getString(R.string.pref_incognito_mode),
NotificationManager.IMPORTANCE_LOW
),
NotificationChannel(
CHANNEL_SIMILAR,
context.getString(R.string.similar_manga),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
}
)
).forEach(context.notificationManager::createNotificationChannel)
// Delete old notification channels

View File

@ -327,12 +327,6 @@ object PreferenceKeys {
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
const val mangadexSimilarEnabled = "pref_related_show_tab_key"
const val mangadexSimilarUpdateInterval = "related_update_interval"
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
const val preferredMangaDexId = "preferred_mangaDex_id"

View File

@ -446,16 +446,8 @@ class PreferencesHelper(val context: Context) {
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
fun mangadexSimilarEnabled() = flowPrefs.getBoolean(Keys.mangadexSimilarEnabled, false)
fun shownMangaDexSimilarAskDialog() = flowPrefs.getBoolean("shown_similar_ask_dialog", false)
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
@ -242,8 +241,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return MangaHandler(baseHttpClient, headers, mdLang.lang).fetchRandomMangaId()
}
suspend fun fetchMangaSimilar(manga: Manga): MangasPage {
return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
suspend fun getMangaSimilar(manga: MangaInfo): MangasPage {
return SimilarHandler(baseHttpClient, mdLang.lang, preferences, useLowQualityThumbnail()).getSimilar(manga)
}
/*private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =

View File

@ -35,7 +35,6 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@ -57,7 +56,6 @@ import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import exh.log.xLogW
import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch
import exh.source.getMainSource
import exh.source.isEhBasedSource
@ -172,11 +170,6 @@ open class BrowseSourceController(bundle: Bundle) :
// SY -->
val mainSource = presenter.source.getMainSource()
if (mainSource is MangaDex && !preferences.mangadexSimilarEnabled().get() && !preferences.shownMangaDexSimilarAskDialog().get()) {
EnableMangaDexSimilarDialogController().showDialog(router)
preferences.shownMangaDexSimilarAskDialog().set(true)
}
if (mainSource is LoginSource && mainSource.requiresLogin && !mainSource.isLogged()) {
val dialog = MangadexLoginDialog(mainSource)
dialog.showDialog(router)

View File

@ -102,7 +102,7 @@ import eu.kanade.tachiyomi.util.view.getCoordinates
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack
import exh.log.xLogD
import exh.md.similar.ui.MangaDexSimilarController
import exh.md.similar.MangaDexSimilarController
import exh.metadata.metadata.base.FlatMetadata
import exh.recs.RecommendsController
import exh.source.MERGED_SOURCE_ID
@ -767,7 +767,7 @@ class MangaController :
// AZ -->
fun openRecommends() {
val source = presenter.source.getMainSource()
if (source is MangaDex && preferences.mangadexSimilarEnabled().get()) {
if (source is MangaDex) {
MaterialDialog(activity!!)
.title(R.string.az_recommends)
.listItemsSingleChoice(

View File

@ -6,27 +6,16 @@ import com.afollestad.materialdialogs.list.listItemsMultiChoice
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.toast
import exh.md.similar.SimilarUpdateJob
import exh.md.utils.MdUtil
import exh.widget.preference.MangaDexLoginPreference
import exh.widget.preference.MangadexLoginDialog
import exh.widget.preference.MangadexLogoutDialog
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
class SettingsMangaDexController :
SettingsController(),
@ -108,76 +97,6 @@ class SettingsMangaDexController :
)
}
}
preferenceCategory {
titleRes = R.string.similar_settings
preference {
key = "pref_similar_screen"
summaryRes = R.string.similar_screen_summary_message
isIconSpaceReserved = true
}
switchPreference {
key = PreferenceKeys.mangadexSimilarEnabled
titleRes = R.string.similar_screen
defaultValue = false
onClick {
SimilarUpdateJob.setupTask(context)
}
}
switchPreference {
key = PreferenceKeys.mangadexSimilarOnlyOverWifi
titleRes = R.string.pref_download_only_over_wifi
defaultValue = true
onClick {
SimilarUpdateJob.setupTask(context, true)
}
}
preference {
key = "pref_similar_manually_update"
titleRes = R.string.similar_manually_update
summaryRes = R.string.similar_manually_update_message
onClick {
SimilarUpdateJob.doWorkNow(context)
context.toast(R.string.similar_manually_toast)
}
}
intListPreference {
key = PreferenceKeys.mangadexSimilarUpdateInterval
titleRes = R.string.similar_update_fequency
entriesRes = arrayOf(
R.string.update_never,
R.string.update_24hour,
R.string.update_48hour,
R.string.update_weekly,
R.string.update_monthly
)
entryValues = arrayOf("0", "1", "2", "7", "30")
defaultValue = "2"
preferences.mangadexSimilarUpdateInterval()
.asImmediateFlow {
SimilarUpdateJob.setupTask(context, true)
}
.drop(1)
.launchIn(viewScope)
}
preference {
key = "similar_credits"
titleRes = R.string.similar_credit
val url = "https://github.com/goldbattle/MangadexRecomendations"
summary = context.getString(R.string.similar_credit_message, url)
onClick {
openInBrowser(url)
}
isIconSpaceReserved = true
}
}
}
override fun siteLoginDialogClosed(source: Source) {

View File

@ -1,36 +1,40 @@
package exh.md.handlers
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.similar.sql.models.MangaSimilarImpl
import exh.md.handlers.serializers.SimilarMangaResponse
import exh.md.utils.MdUtil
import exh.util.executeOnIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import tachiyomi.source.model.MangaInfo
class SimilarHandler(val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
class SimilarHandler(val client: OkHttpClient, val lang: String, val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
/**
* fetch our similar mangas
*/
suspend fun fetchSimilar(manga: Manga): MangasPage {
// Parse the Mangadex id from the URL
val mangaId = MdUtil.getMangaId(manga.url).toLong()
val similarMangaDb = Injekt.get<DatabaseHelper>().getSimilar(mangaId).executeOnIO()
return if (similarMangaDb != null) {
val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
SManga.create().apply {
title = similarMangaTitles[index]
url = "/manga/$similarId/"
thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers)
}
suspend fun getSimilar(manga: MangaInfo): MangasPage {
val response = client.newCall(similarMangaRequest(manga)).await()
return similarMangaParse(response)
}
private fun similarMangaRequest(manga: MangaInfo): Request {
val tempUrl = MdUtil.similarBaseApi + MdUtil.getMangaId(manga.key) + ".json"
return GET(tempUrl, Headers.Builder().build(), CacheControl.FORCE_NETWORK)
}
private fun similarMangaParse(response: Response): MangasPage {
val mangaList = response.parseAs<SimilarMangaResponse>().matches.map {
SManga.create().apply {
url = "/manga/" + it.id
title = MdUtil.cleanString(it.title[lang] ?: it.title["en"]!!)
thumbnail_url = "https://coverapi.orell.dev/api/v1/mdaltimage/manga/${it.id}/cover"
}
MangasPage(similarMangas, false)
} else MangasPage(mutableListOf(), false)
}
return MangasPage(mangaList, false)
}
}

View File

@ -0,0 +1,20 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class SimilarMangaResponse(
val id: String,
val title: Map<String, String>,
val contentRating: String,
val matches: List<Matches>,
val updatedAt: String
)
@Serializable
data class Matches(
val id: String,
val title: Map<String, String>,
val contentRating: String,
val score: Double
)

View File

@ -1,4 +1,4 @@
package exh.md.similar.ui
package exh.md.similar
import android.os.Bundle
import android.view.Menu

View File

@ -1,6 +1,7 @@
package exh.md.similar.ui
package exh.md.similar
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
@ -16,7 +17,7 @@ import rx.schedulers.Schedulers
class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
override fun requestNext(): Observable<MangasPage> {
return runAsObservable({ source.fetchMangaSimilar(manga) })
return runAsObservable({ source.getMangaSimilar(manga.toMangaInfo()) })
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {

View File

@ -1,4 +1,4 @@
package exh.md.similar.ui
package exh.md.similar
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga

View File

@ -1,74 +0,0 @@
package exh.md.similar
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class SimilarUpdateJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
SimilarUpdateService.start(context)
return Result.success()
}
companion object {
const val TAG = "RelatedUpdate"
fun setupTask(context: Context, skipInitial: Boolean = false) {
val preferences = Injekt.get<PreferencesHelper>()
val enabled = preferences.mangadexSimilarEnabled().get()
val interval = preferences.mangadexSimilarUpdateInterval().get()
if (enabled) {
// We are enabled, so construct the constraints
val wifiRestriction = if (preferences.mangadexSimilarOnlyOverWifi().get()) {
NetworkType.UNMETERED
} else {
NetworkType.CONNECTED
}
val constraints = Constraints.Builder()
.setRequiredNetworkType(wifiRestriction)
.build()
// If we are not skipping the initial then run it right now
// Note that we won't run it if the constraints are not satisfied
if (!skipInitial) {
WorkManager.getInstance(context).enqueue(OneTimeWorkRequestBuilder<SimilarUpdateJob>().setConstraints(constraints).build())
}
// Finally build the periodic request
val request = PeriodicWorkRequestBuilder<SimilarUpdateJob>(
interval.toLong(),
TimeUnit.DAYS,
1,
TimeUnit.HOURS
)
.addTag(TAG)
.setConstraints(constraints)
.build()
if (interval > 0) {
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
} else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
}
fun doWorkNow(context: Context) {
WorkManager.getInstance(context).enqueue(OneTimeWorkRequestBuilder<SimilarUpdateJob>().build())
}
}
}

View File

@ -1,351 +0,0 @@
package exh.md.similar
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.text.isDigitsOnly
import com.squareup.moshi.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.notificationManager
import exh.log.xLogE
import exh.md.similar.sql.models.MangaSimilarImpl
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import okio.buffer
import okio.sink
import okio.source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class SimilarUpdateService(
val db: DatabaseHelper = Injekt.get()
) : Service() {
private val client by lazy {
Injekt.get<NetworkHelper>().client.newBuilder()
// unzip interceptor which will add the correct headers
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.build()
}
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private val similarServiceScope = CoroutineScope(Dispatchers.IO + Job())
/**
* Subscription where the update is done.
*/
private var job: Job? = null
/**
* Pending intent of action that cancels the library update
*/
private val cancelIntent by lazy {
NotificationReceiver.cancelSimilarUpdatePendingBroadcast(this)
}
private val progressNotification by lazy {
NotificationCompat.Builder(this, Notifications.CHANNEL_SIMILAR)
.setLargeIcon(BitmapFactory.decodeResource(this.resources, R.mipmap.ic_launcher))
.setSmallIcon(R.drawable.ic_tachi)
.setOngoing(true)
.setContentTitle(getString(R.string.similar_loading_progress_start))
.setAutoCancel(true)
.addAction(
R.drawable.ic_close_24dp,
getString(android.R.string.cancel),
cancelIntent
)
}
/**
* Method called when the service is created. It injects dagger dependencies and acquire
* the wake lock.
*/
override fun onCreate() {
super.onCreate()
wakeLock = acquireWakeLock("SimilarUpdateService")
startForeground(Notifications.ID_SIMILAR_PROGRESS, progressNotification.build())
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
super.onDestroy()
}
private fun destroyJob() {
job?.cancel()
if (similarServiceScope.isActive) similarServiceScope.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
/**
* Method called when the service receives an intent.
*
* @param intent the start intent from.
* @param flags the flags of the command.
* @param startId the start id of this command.
* @return the start value of the command.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
job?.cancel()
val handler = CoroutineExceptionHandler { _, exception ->
xLogE("Similar manga update error", exception)
stopSelf(startId)
showResultNotification(true)
cancelProgressNotification()
}
job = similarServiceScope.launch(handler) {
updateSimilar()
}
job?.invokeOnCompletion { stopSelf(startId) }
return START_REDELIVER_INTENT
}
/**
* Method that updates the similar database for manga
*/
private suspend fun updateSimilar() = withIOContext {
val response = client
.newCall(GET(similarUrl))
.await()
if (!response.isSuccessful) {
throw Exception("Error trying to download similar file")
}
val destinationFile = File(filesDir, "neko-similar.json")
val buffer = withIOContext { destinationFile.sink().buffer() }
// write json to file
response.body?.byteStream()?.source()?.use { input ->
buffer.use { output ->
output.writeAll(input)
}
}
val listSimilar = getSimilar(destinationFile)
// Delete the old similar table
db.deleteAllSimilar().executeAsBlocking()
val totalManga = listSimilar.size
// Loop through each and insert into the database
val dataToInsert = listSimilar.mapIndexed { index, similarFromJson ->
showProgressNotification(index, totalManga)
if (similarFromJson.similarIds.size != similarFromJson.similarTitles.size) {
return@mapIndexed null
}
MangaSimilarImpl().apply {
id = index.toLong()
manga_id = similarFromJson.id.toLong()
matched_ids = similarFromJson.similarIds.joinToString(MangaSimilarImpl.DELIMITER)
matched_titles = similarFromJson.similarTitles.joinToString(MangaSimilarImpl.DELIMITER)
}
}.filterNotNull()
showProgressNotification(dataToInsert.size, totalManga)
if (dataToInsert.isNotEmpty()) {
db.insertSimilar(dataToInsert).executeAsBlocking()
}
destinationFile.delete()
showResultNotification(!this.isActive)
cancelProgressNotification()
}
private fun getSimilar(destinationFile: File): List<SimilarFromJson> {
val reader = JsonReader.of(destinationFile.source().buffer())
var processingManga = false
var processingTitles = false
var mangaId: String? = null
var similarIds = mutableListOf<String>()
var similarTitles = mutableListOf<String>()
val similars = mutableListOf<SimilarFromJson>()
while (reader.peek() != JsonReader.Token.END_DOCUMENT) {
when (reader.peek()) {
JsonReader.Token.BEGIN_OBJECT -> {
reader.beginObject()
}
JsonReader.Token.NAME -> {
val name = reader.nextName()
if (!processingManga && name.isDigitsOnly()) {
processingManga = true
// similar add id
mangaId = name
} else if (name == "m_titles") {
processingTitles = true
}
}
JsonReader.Token.BEGIN_ARRAY -> {
reader.beginArray()
}
JsonReader.Token.END_ARRAY -> {
reader.endArray()
if (processingTitles) {
processingManga = false
processingTitles = false
similars.add(SimilarFromJson(mangaId!!, similarIds.toList(), similarTitles.toList()))
mangaId = null
similarIds = mutableListOf()
similarTitles = mutableListOf()
}
}
JsonReader.Token.NUMBER -> {
similarIds.add(reader.nextInt().toString())
}
JsonReader.Token.STRING -> {
if (processingTitles) {
similarTitles.add(reader.nextString())
}
}
JsonReader.Token.END_OBJECT -> {
reader.endObject()
}
else -> Unit
}
}
return similars
}
data class SimilarFromJson(val id: String, val similarIds: List<String>, val similarTitles: List<String>)
/**
* Shows the notification containing the currently updating manga and the progress.
*
* @param current the current progress.
* @param total the total progress.
*/
private fun showProgressNotification(current: Int, total: Int) {
notificationManager.notify(
Notifications.ID_SIMILAR_PROGRESS,
progressNotification
.setContentTitle(
getString(
R.string.similar_loading_percent,
current,
total
)
)
.setProgress(total, current, false)
.build()
)
}
/**
* Shows the notification containing the result of the update done by the service.
*
* @param error if the result was a error.
*/
private fun showResultNotification(error: Boolean = false) {
val title = if (error) {
getString(R.string.similar_loading_complete_error)
} else {
getString(
R.string.similar_loading_complete
)
}
val result = NotificationCompat.Builder(this, Notifications.CHANNEL_SIMILAR)
.setContentTitle(title)
.setLargeIcon(BitmapFactory.decodeResource(this.resources, R.mipmap.ic_launcher))
.setSmallIcon(R.drawable.ic_tachi)
.setAutoCancel(true)
NotificationManagerCompat.from(this)
.notify(Notifications.ID_SIMILAR_COMPLETE, result.build())
}
/**
* Cancels the progress notification.
*/
private fun cancelProgressNotification() {
notificationManager.cancel(Notifications.ID_SIMILAR_PROGRESS)
}
companion object {
private const val similarUrl = "https://raw.githubusercontent.com/goldbattle/MangadexRecomendations/master/output/mangas_compressed.json.gz"
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return context.isServiceRunning(SimilarUpdateService::class.java)
}
/**
* Starts the service. It will be started only if there isn't another instance already
* running.
*
* @param context the application context.
*/
fun start(context: Context) {
if (!isRunning(context)) {
val intent = Intent(context, SimilarUpdateService::class.java)
ContextCompat.startForegroundService(context, intent)
}
}
/**
* Stops the service.
*
* @param context the application context.
*/
fun stop(context: Context) {
context.stopService(Intent(context, SimilarUpdateService::class.java))
}
}
}

View File

@ -1,63 +0,0 @@
package exh.md.similar.sql.mappers
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import exh.md.similar.sql.models.MangaSimilar
import exh.md.similar.sql.models.MangaSimilarImpl
import exh.md.similar.sql.tables.SimilarTable.COL_ID
import exh.md.similar.sql.tables.SimilarTable.COL_MANGA_ID
import exh.md.similar.sql.tables.SimilarTable.COL_MANGA_SIMILAR_MATCHED_IDS
import exh.md.similar.sql.tables.SimilarTable.COL_MANGA_SIMILAR_MATCHED_TITLES
import exh.md.similar.sql.tables.SimilarTable.TABLE
class SimilarTypeMapping : SQLiteTypeMapping<MangaSimilar>(
SimilarPutResolver(),
SimilarGetResolver(),
SimilarDeleteResolver()
)
class SimilarPutResolver : DefaultPutResolver<MangaSimilar>() {
override fun mapToInsertQuery(obj: MangaSimilar) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaSimilar) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaSimilar) = contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_MANGA_SIMILAR_MATCHED_IDS to obj.matched_ids,
COL_MANGA_SIMILAR_MATCHED_TITLES to obj.matched_titles
)
}
class SimilarGetResolver : DefaultGetResolver<MangaSimilar>() {
override fun mapFromCursor(cursor: Cursor): MangaSimilar = MangaSimilarImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
matched_ids = cursor.getString(cursor.getColumnIndex(COL_MANGA_SIMILAR_MATCHED_IDS))
matched_titles = cursor.getString(cursor.getColumnIndex(COL_MANGA_SIMILAR_MATCHED_TITLES))
}
}
class SimilarDeleteResolver : DefaultDeleteResolver<MangaSimilar>() {
override fun mapToDeleteQuery(obj: MangaSimilar) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -1,31 +0,0 @@
package exh.md.similar.sql.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
interface MangaSimilar : Serializable {
/**
* Id of this similar manga object.
*/
var id: Long?
/**
* Id of matching manga
*/
var manga_id: Long?
/**
* JSONArray.toString() list of ids for this manga
* Example: [3467, 5907, 21052, 2141, 6139, 5602, 3999]
*/
var matched_ids: String
/**
* JSONArray.toString() list of titles for this manga
* Example: [Title1, Title2, ..., Title10]
*/
var matched_titles: String
}

View File

@ -1,32 +0,0 @@
package exh.md.similar.sql.models
class MangaSimilarImpl : MangaSimilar {
override var id: Long? = null
override var manga_id: Long? = null
override lateinit var matched_ids: String
override lateinit var matched_titles: String
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
other as MangaSimilar
if (id != other.id) return false
if (manga_id != other.manga_id) return false
if (matched_ids != other.matched_ids) return false
return matched_titles != other.matched_titles
}
override fun hashCode(): Int {
return id.hashCode() + manga_id.hashCode()
}
companion object {
const val DELIMITER = "|*|"
}
}

View File

@ -1,42 +0,0 @@
package exh.md.similar.sql.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import exh.md.similar.sql.models.MangaSimilar
import exh.md.similar.sql.tables.SimilarTable
interface SimilarQueries : DbProvider {
fun getAllSimilar() = db.get()
.listOfObjects(MangaSimilar::class.java)
.withQuery(
Query.builder()
.table(SimilarTable.TABLE)
.build()
)
.prepare()
fun getSimilar(manga_id: Long) = db.get()
.`object`(MangaSimilar::class.java)
.withQuery(
Query.builder()
.table(SimilarTable.TABLE)
.where("${SimilarTable.COL_MANGA_ID} = ?")
.whereArgs(manga_id)
.build()
)
.prepare()
fun insertSimilar(similar: MangaSimilar) = db.put().`object`(similar).prepare()
fun insertSimilar(similarList: List<MangaSimilar>) = db.put().objects(similarList).prepare()
fun deleteAllSimilar() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(SimilarTable.TABLE)
.build()
)
.prepare()
}

View File

@ -1,27 +0,0 @@
package exh.md.similar.sql.tables
object SimilarTable {
const val TABLE = "manga_related"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_MANGA_SIMILAR_MATCHED_IDS = "matched_ids"
const val COL_MANGA_SIMILAR_MATCHED_TITLES = "matched_titles"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_MANGA_SIMILAR_MATCHED_IDS TEXT NOT NULL,
$COL_MANGA_SIMILAR_MATCHED_TITLES TEXT NOT NULL,
UNIQUE ($COL_ID) ON CONFLICT REPLACE
)"""
val createMangaIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_SIMILAR_MATCHED_IDS}_index ON $TABLE($COL_MANGA_SIMILAR_MATCHED_IDS)"
}

View File

@ -1,27 +0,0 @@
package exh.md.similar.ui
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import exh.md.similar.SimilarUpdateJob
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class EnableMangaDexSimilarDialogController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
val preferences = Injekt.get<PreferencesHelper>()
return MaterialDialog(activity)
.title(text = activity.getString(R.string.similar_ask_to_enable_title))
.message(R.string.similar_ask_to_enable)
.negativeButton(R.string.similar_ask_to_enable_no, activity.getString(R.string.similar_ask_to_enable_no))
.positiveButton(R.string.similar_ask_to_enable_yes, activity.getString(R.string.similar_ask_to_enable_yes)) {
preferences.mangadexSimilarEnabled().set(true)
SimilarUpdateJob.setupTask(activity)
}
}
}

View File

@ -42,8 +42,6 @@ class MdUtil {
const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org"
const val baseUrl = "https://mangadex.org"
const val apiUrl = "https://api.mangadex.org"
const val apiUrlCdnCache = "https://cdn.statically.io/gh/goldbattle/MangadexRecomendations/master/output/api/"
const val apiUrlCache = "https://raw.githubusercontent.com/goldbattle/MangadexRecomendations/master/output/api/"
const val imageUrlCacheNotFound = "https://cdn.statically.io/img/raw.githubusercontent.com/CarlosEsco/Neko/master/.github/manga_cover_not_found.png"
const val atHomeUrl = "$apiUrl/at-home/server"
const val chapterUrl = "$apiUrl/chapter/"
@ -70,6 +68,10 @@ class MdUtil {
}.build().toString()
}
const val similarCache = "https://raw.githubusercontent.com/goldbattle/MangadexRecomendations/master/output/api/"
const val similarCacheCdn = "https://cdn.statically.io/gh/goldbattle/MangadexRecomendations/master/output/api/"
const val similarBaseApi = "https://api.similarmanga.com/similar/"
const val groupSearchUrl = "$baseUrl/groups/0/1/"
const val apiCovers = "/covers"
const val reportUrl = "https://api.mangadex.network/report"