lots of background work for manga extensions

This commit is contained in:
Finnley Somdahl 2023-10-18 23:52:03 -05:00
parent dbe573131e
commit 57a584a820
123 changed files with 2676 additions and 553 deletions

View file

@ -9,8 +9,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" android:maxSdkVersion="32" />
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- For background jobs --> <!-- For background jobs -->

View file

@ -8,8 +8,8 @@ import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import ani.dantotsu.aniyomi.anime.custom.AppModule import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import ani.dantotsu.aniyomi.data.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
import ani.dantotsu.others.DisabledReports import ani.dantotsu.others.DisabledReports
import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase

View file

@ -24,7 +24,7 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
@ -37,13 +37,16 @@ import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
@ -58,6 +61,7 @@ class MainActivity : AppCompatActivity() {
private var uiSettings = UserInterfaceSettings() private var uiSettings = UserInterfaceSettings()
private val animeExtensionManager: AnimeExtensionManager by injectLazy() private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -65,11 +69,17 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val myScope = CoroutineScope(Dispatchers.Default) val animeScope = CoroutineScope(Dispatchers.Default)
myScope.launch { animeScope.launch {
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow) AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
} }
var doubleBackToExitPressedOnce = false var doubleBackToExitPressedOnce = false

View file

@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -1,3 +0,0 @@
NOTICE
This software includes code modified from Aniyomi, available at https://github.com/aniyomiorg/aniyomi/.

View file

@ -1,30 +0,0 @@
package ani.dantotsu.aniyomi.anime.custom
/*
import android.app.Application
import ani.dantotsu.aniyomi.data.Notifications
import ani.dantotsu.aniyomi.util.logcat
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import uy.kohesive.injekt.Injekt
class App : Application() {
override fun onCreate() {
super<Application>.onCreate()
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
}
private fun setupNotificationChannels() {
try {
Notifications.createChannels(this)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
}
}
}*/

View file

@ -1,11 +1,12 @@
package ani.dantotsu.aniyomi.anime.custom package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import ani.dantotsu.aniyomi.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import ani.dantotsu.aniyomi.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import ani.dantotsu.aniyomi.core.preference.AndroidPreferenceStore import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
@ -22,6 +23,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory { addSingletonFactory {
Json { Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true

View file

@ -1,3 +0,0 @@
package ani.dantotsu.aniyomi.util.srcapi
//actual suspend fun <T> Observable<T>.awaitSingle(): T = awaitSingle()

View file

@ -238,7 +238,7 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean { suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
return tryWithSuspend(true) { return tryWithSuspend(true) {
chapter.addImages( chapter.addImages(
mangaReadSources?.get(selected.source)?.loadImages(chapter.link) ?: return@tryWithSuspend false mangaReadSources?.get(selected.source)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
) )
if (post) mangaChapter.postValue(chapter) if (post) mangaChapter.postValue(chapter)
true true

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media.manga
import ani.dantotsu.parsers.MangaChapter import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaImage
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable import java.io.Serializable
import kotlin.math.floor import kotlin.math.floor
@ -10,8 +11,9 @@ data class MangaChapter(
var link: String, var link: String,
var title: String? = null, var title: String? = null,
var description: String? = null, var description: String? = null,
var sChapter: SChapter
) : Serializable { ) : Serializable {
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description) constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)
private val images = mutableListOf<MangaImage>() private val images = mutableListOf<MangaImage>()
fun images(): List<MangaImage> = images fun images(): List<MangaImage> = images

View file

@ -25,7 +25,7 @@ object Jikan {
val ep = it.malID.toString() val ep = it.malID.toString()
eps[ep] = Episode(ep, title = it.title, eps[ep] = Episode(ep, title = it.title,
//Personal revenge with 34566 :prayge: //Personal revenge with 34566 :prayge:
filler = if(malId!=34566) it.filler else true filler = if(malId!=34566) it.filler else true,
) )
} }
hasNextPage = res?.pagination?.hasNextPage == true hasNextPage = res?.pagination?.hasNextPage == true

View file

@ -1,34 +1,11 @@
package ani.dantotsu.parsers package ani.dantotsu.parsers
import ani.dantotsu.Lazier import ani.dantotsu.Lazier
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.lazyList import ani.dantotsu.lazyList
//import ani.dantotsu.parsers.anime.AllAnime
//import ani.dantotsu.parsers.anime.AnimeDao
//import ani.dantotsu.parsers.anime.AnimePahe
//import ani.dantotsu.parsers.anime.Gogo
//import ani.dantotsu.parsers.anime.Haho
//import ani.dantotsu.parsers.anime.HentaiFF
//import ani.dantotsu.parsers.anime.HentaiMama
//import ani.dantotsu.parsers.anime.HentaiStream
//import ani.dantotsu.parsers.anime.Marin
//import ani.dantotsu.parsers.anime.AniWave
//import ani.dantotsu.parsers.anime.Kaido
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
/*
object AnimeSources_old : WatchSources() {
override val list: List<Lazier<BaseParser>> = lazyList(
"AllAnime" to ::AllAnime,
"Gogo" to ::Gogo,
"Kaido" to ::Kaido,
"Marin" to ::Marin,
"AnimePahe" to ::AnimePahe,
"AniWave" to ::AniWave,
"AnimeDao" to ::AnimeDao,
)
}
*/
object AnimeSources : WatchSources() { object AnimeSources : WatchSources() {
override var list: List<Lazier<BaseParser>> = emptyList() override var list: List<Lazier<BaseParser>> = emptyList()
@ -52,13 +29,8 @@ object AnimeSources : WatchSources() {
} }
object HAnimeSources : WatchSources() { object HAnimeSources : WatchSources() {
private val aList: List<Lazier<BaseParser>> = lazyList( private val aList: List<Lazier<BaseParser>> = lazyList(
//"HentaiMama" to ::HentaiMama,
//"Haho" to ::Haho,
//"HentaiStream" to ::HentaiStream,
//"HentaiFF" to ::HentaiFF,
) )
override val list = listOf(aList,AnimeSources.list).flatten() override val list = listOf(aList,AnimeSources.list).flatten()

View file

@ -1,10 +1,18 @@
package ani.dantotsu.parsers package ani.dantotsu.parsers
import android.content.ContentResolver
import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast import android.widget.Toast
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import ani.dantotsu.aniyomi.util.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.logger import ani.dantotsu.logger
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
@ -13,6 +21,22 @@ import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.awaitSingle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
@ -91,7 +115,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
} }
fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> { private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> {
return animesPage.animes.map { sAnime -> return animesPage.animes.map { sAnime ->
// Extract required fields from sAnime // Extract required fields from sAnime
val name = sAnime.title val name = sAnime.title
@ -106,34 +130,160 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} }
} }
fun SEpisodeToEpisode(sEpisode: SEpisode): Episode { private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
val episode = Episode( //if the float episode number is a whole number, convert it to an int
sEpisode.episode_number.toString(), val episodeNumberInt =
if (sEpisode.episode_number % 1 == 0f) {
sEpisode.episode_number.toInt()
} else {
sEpisode.episode_number
}
return Episode(
episodeNumberInt.toString(),
sEpisode.url, sEpisode.url,
sEpisode.name, sEpisode.name,
null, null,
null, null,
false, false,
null, null,
sEpisode) sEpisode
return episode )
} }
fun VideoToVideoServer(video: Video): VideoServer { private fun VideoToVideoServer(video: Video): VideoServer {
val videoServer = VideoServer( return VideoServer(
video.quality, video.quality,
video.url, video.url,
null, null,
video) video
return videoServer )
} }
} }
class VideoServerPassthrough : VideoExtractor{ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val videoServer: VideoServer val extension: MangaExtension.Installed
constructor(videoServer: VideoServer) { init {
this.videoServer = videoServer this.extension = extension
} }
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
val source = extension.sources.first()
if (source is CatalogueSource) {
try {
val res = source.getChapterList(sManga)
var chapterList: List<MangaChapter> = emptyList()
for (chapter in res) {
chapterList += SChapterToMangaChapter(chapter)
}
logger("chapterList size: ${chapterList.size}")
return chapterList
}
catch (e: Exception) {
logger("loadChapters Exception: $e")
}
return emptyList()
}
return emptyList() // Return an empty list if source is not a catalogueSource
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val source = extension.sources.first()
if (source is HttpSource) {
//try {
val res = source.getPageList(sChapter)
var chapterList: List<MangaImage> = emptyList()
for (page in res) {
println("page: $page")
currContext()?.let { fetchAndProcessImage(page, source, it.contentResolver) }
logger("new image url: ${page.imageUrl}")
chapterList += PageToMangaImage(page)
}
logger("image url: chapterList size: ${chapterList.size}")
return chapterList
//}
//catch (e: Exception) {
// logger("loadImages Exception: $e")
//}
return emptyList()
}
return emptyList() // Return an empty list if source is not a CatalogueSource
}
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first()
if (source is HttpSource) {
var res: MangasPage? = null
try {
res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
logger("res observable: $res")
}
catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
}
val conv = convertMangasPageToShowResponse(res!!)
return conv
}
return emptyList() // Return an empty list if source is not a CatalogueSource
}
private fun convertMangasPageToShowResponse(mangasPage: MangasPage): List<ShowResponse> {
return mangasPage.mangas.map { sManga ->
// Extract required fields from sManga
val name = sManga.title
val link = sManga.url
val coverUrl = sManga.thumbnail_url ?: ""
val otherNames = emptyList<String>() // Populate as needed
val total = 20
val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse
ShowResponse(name, link, coverUrl, sManga)
}
}
private fun PageToMangaImage(page: Page): MangaImage {
//find and move any headers from page.imageUrl to headersMap
val headersMap: Map<String, String> = page.imageUrl?.split("&")?.mapNotNull {
val idx = it.indexOf("=")
if (idx != -1) {
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8")
Pair(key, value)
} else {
null // Or some other default value
}
}?.toMap() ?: mapOf()
val urlWithoutHeaders = page.imageUrl?.split("&")?.get(0) ?: ""
val url = page.imageUrl ?: ""
logger("Pageurl: $url")
logger("regularurl: ${page.url}")
logger("regularurl: ${page.status}")
return MangaImage(
FileUrl(url, headersMap),
false,
page
)
}
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
return MangaChapter(
sChapter.name,
sChapter.url,
sChapter.name,
null,
sChapter
)
}
}
class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer override val server: VideoServer
get() { get() {
return videoServer return videoServer

View file

@ -3,6 +3,7 @@ package ani.dantotsu.parsers
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
import java.io.Serializable import java.io.Serializable
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
@ -141,7 +142,10 @@ data class ShowResponse(
val extra : Map<String,String>?=null, val extra : Map<String,String>?=null,
//SAnime object from Aniyomi //SAnime object from Aniyomi
val sAnime: SAnime?=null val sAnime: SAnime? = null,
//SManga object from Aniyomi
val sManga: SManga? = null
) : Serializable { ) : Serializable {
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf(), total: Int? = null, extra: Map<String, String>?=null) constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf(), total: Int? = null, extra: Map<String, String>?=null)
: this(name, link, FileUrl(coverUrl), otherNames, total, extra) : this(name, link, FileUrl(coverUrl), otherNames, total, extra)
@ -157,6 +161,9 @@ data class ShowResponse(
constructor(name: String, link: String, coverUrl: String, sAnime: SAnime) constructor(name: String, link: String, coverUrl: String, sAnime: SAnime)
: this(name, link, FileUrl(coverUrl), sAnime = sAnime) : this(name, link, FileUrl(coverUrl), sAnime = sAnime)
constructor(name: String, link: String, coverUrl: String, sManga: SManga)
: this(name, link, FileUrl(coverUrl), sManga = sManga)
} }

View file

@ -1,6 +1,7 @@
package ani.dantotsu.parsers package ani.dantotsu.parsers
import ani.dantotsu.Lazier import ani.dantotsu.Lazier
import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -52,11 +53,17 @@ abstract class MangaReadSources : BaseSources() {
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> { suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
val map = mutableMapOf<String, MangaChapter>() val map = mutableMapOf<String, MangaChapter>()
val parser = get(i) val parser = get(i)
show.sManga?.let { sManga ->
tryWithSuspend(true) { tryWithSuspend(true) {
parser.loadChapters(show.link, show.extra).forEach { parser.loadChapters(show.link, show.extra, sManga).forEach {
map[it.number] = MangaChapter(it) map[it.number] = MangaChapter(it)
} }
} }
}
if(show.sManga == null) {
logger("sManga is null")
}
logger("map size ${map.size}")
return map return map
} }
} }

View file

@ -3,6 +3,9 @@ package ani.dantotsu.parsers
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import java.io.Serializable import java.io.Serializable
abstract class MangaParser : BaseParser() { abstract class MangaParser : BaseParser() {
@ -10,7 +13,7 @@ abstract class MangaParser : BaseParser() {
/** /**
* Takes ShowResponse.link and ShowResponse.extra (if any) as arguments & gives a list of total chapters present on the site. * Takes ShowResponse.link and ShowResponse.extra (if any) as arguments & gives a list of total chapters present on the site.
* **/ * **/
abstract suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?): List<MangaChapter> abstract suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter>
/** /**
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Chapter Number known by app as arguments * Takes ShowResponse.link, ShowResponse.extra & the Last Largest Chapter Number known by app as arguments
@ -18,8 +21,8 @@ abstract class MangaParser : BaseParser() {
* Returns the latest chapter (If overriding, Make sure the chapter is actually the latest chapter) * Returns the latest chapter (If overriding, Make sure the chapter is actually the latest chapter)
* Returns null, if no latest chapter is found. * Returns null, if no latest chapter is found.
* **/ * **/
open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, latest: Float): MangaChapter? { open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, sManga: SManga, latest: Float): MangaChapter? {
return loadChapters(mangaLink, extra) return loadChapters(mangaLink, extra, sManga)
.maxByOrNull { it.number.toFloatOrNull() ?: 0f } .maxByOrNull { it.number.toFloatOrNull() ?: 0f }
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) } ?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
} }
@ -27,7 +30,7 @@ abstract class MangaParser : BaseParser() {
/** /**
* Takes MangaChapter.link as an argument & returns a list of MangaImages with their Url (with headers & transformations, if needed) * Takes MangaChapter.link as an argument & returns a list of MangaImages with their Url (with headers & transformations, if needed)
* **/ * **/
abstract suspend fun loadImages(chapterLink: String): List<MangaImage> abstract suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage>
override suspend fun autoSearch(mediaObj: Media): ShowResponse? { override suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id) var response = loadSavedShowResponse(mediaObj.id)
@ -65,6 +68,8 @@ data class MangaChapter(
//Self-Descriptive //Self-Descriptive
val title: String? = null, val title: String? = null,
val description: String? = null, val description: String? = null,
val sChapter: SChapter,
) )
data class MangaImage( data class MangaImage(
@ -75,8 +80,10 @@ data class MangaImage(
* **/ * **/
val url: FileUrl, val url: FileUrl,
val useTransformation: Boolean = false val useTransformation: Boolean = false,
val page: Page
) : Serializable{ ) : Serializable{
constructor(url: String,useTransformation: Boolean=false) constructor(url: String,useTransformation: Boolean=false, page: Page)
: this(FileUrl(url),useTransformation) : this(FileUrl(url),useTransformation, page)
} }

View file

@ -2,10 +2,30 @@ package ani.dantotsu.parsers
import ani.dantotsu.Lazier import ani.dantotsu.Lazier
import ani.dantotsu.lazyList import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() { object MangaSources : MangaReadSources() {
override val list: List<Lazier<BaseParser>> = lazyList( override var list: List<Lazier<BaseParser>> = emptyList()
)
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions)
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions)
}
}
private fun createParsersFromExtensions(extensions: List<MangaExtension.Installed>): List<Lazier<BaseParser>> {
return extensions.map { extension ->
val name = extension.name
Lazier({ DynamicMangaParser(extension) }, name)
}
}
} }
object HMangaSources : MangaReadSources() { object HMangaSources : MangaReadSources() {

View file

@ -3,13 +3,11 @@ package ani.dantotsu.settings
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build.* import android.os.Build.*
import android.os.Build.VERSION.* import android.os.Build.VERSION.*
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -19,16 +17,19 @@ import android.widget.SearchView
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.databinding.ActivityExtensionsBinding import ani.dantotsu.databinding.ActivityExtensionsBinding
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -60,7 +61,7 @@ class ExtensionsActivity : AppCompatActivity() {
.subscribe( .subscribe(
{ installStep -> { installStep ->
val builder = NotificationCompat.Builder(this, val builder = NotificationCompat.Builder(this,
ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_PROGRESS Notifications.CHANNEL_DOWNLOADER_PROGRESS
) )
.setSmallIcon(R.drawable.ic_round_sync_24) .setSmallIcon(R.drawable.ic_round_sync_24)
.setContentTitle("Installing extension") .setContentTitle("Installing extension")
@ -70,7 +71,7 @@ class ExtensionsActivity : AppCompatActivity() {
}, },
{ error -> { error ->
val builder = NotificationCompat.Builder(this, val builder = NotificationCompat.Builder(this,
ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_ERROR Notifications.CHANNEL_DOWNLOADER_ERROR
) )
.setSmallIcon(R.drawable.ic_round_info_24) .setSmallIcon(R.drawable.ic_round_info_24)
.setContentTitle("Installation failed") .setContentTitle("Installation failed")
@ -80,7 +81,7 @@ class ExtensionsActivity : AppCompatActivity() {
}, },
{ {
val builder = NotificationCompat.Builder(this, val builder = NotificationCompat.Builder(this,
ani.dantotsu.aniyomi.data.Notifications.CHANNEL_DOWNLOADER_PROGRESS) Notifications.CHANNEL_DOWNLOADER_PROGRESS)
.setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check)
.setContentTitle("Installation complete") .setContentTitle("Installation complete")
.setContentText("The extension has been successfully installed.") .setContentText("The extension has been successfully installed.")
@ -164,8 +165,11 @@ class ExtensionsActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
} }
private class ExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter<ExtensionsAdapter.ViewHolder>() { private class ExtensionsAdapter(private val onUninstallClicked: (String) -> Unit) : RecyclerView.Adapter<ExtensionsAdapter.ViewHolder>() {
private var extensions: List<AnimeExtension.Installed> = emptyList() private var extensions: List<AnimeExtension.Installed> = emptyList()

View file

@ -66,7 +66,10 @@ class SubscriptionHelper {
val chp = withTimeoutOrNull(10 * 1000) { val chp = withTimeoutOrNull(10 * 1000) {
tryWithSuspend { tryWithSuspend {
val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id)) val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id))
parser.getLatestChapter(show.link, show.extra, selected.latest) show.sManga?.let {
parser.getLatestChapter(show.link, show.extra,
it, selected.latest)
}
} }
} }

View file

@ -0,0 +1,38 @@
package eu.kanade.core.preference
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import tachiyomi.core.preference.Preference
class PreferenceMutableState<T>(
private val preference: Preference<T>,
scope: CoroutineScope,
) : MutableState<T> {
private val state = mutableStateOf(preference.get())
init {
preference.changes()
.onEach { state.value = it }
.launchIn(scope)
}
override var value: T
get() = state.value
set(value) {
preference.set(value)
}
override fun component1(): T {
return state.value
}
override fun component2(): (T) -> Unit {
return { preference.set(it) }
}
}
fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)

View file

@ -1,9 +1,9 @@
package ani.dantotsu.aniyomi.domain.base package eu.kanade.domain.base
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import ani.dantotsu.aniyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
class BasePreferences( class BasePreferences(
val context: Context, val context: Context,

View file

@ -1,13 +1,13 @@
package ani.dantotsu.aniyomi.domain.base package eu.kanade.domain.base
import android.content.Context import android.content.Context
import ani.dantotsu.aniyomi.util.system.hasMiuiPackageInstaller import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import ani.dantotsu.aniyomi.domain.base.BasePreferences.ExtensionInstaller import eu.kanade.domain.base.BasePreferences.ExtensionInstaller
import ani.dantotsu.aniyomi.util.system.isShizukuInstalled import eu.kanade.tachiyomi.util.system.isShizukuInstalled
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import ani.dantotsu.aniyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import ani.dantotsu.aniyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import ani.dantotsu.aniyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
class ExtensionInstallerPreference( class ExtensionInstallerPreference(
private val context: Context, private val context: Context,

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.domain.source.service package eu.kanade.domain.source.service
class SetMigrateSorting( class SetMigrateSorting(
private val preferences: SourcePreferences, private val preferences: SourcePreferences,

View file

@ -1,9 +1,9 @@
package ani.dantotsu.aniyomi.domain.source.service package eu.kanade.domain.source.service
import ani.dantotsu.aniyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.util.system.LocaleHelper
import ani.dantotsu.aniyomi.util.system.LocaleHelper import tachiyomi.core.preference.PreferenceStore
import ani.dantotsu.aniyomi.core.preference.getEnum import tachiyomi.core.preference.getEnum
import ani.dantotsu.aniyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
class SourcePreferences( class SourcePreferences(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,

View file

@ -0,0 +1,15 @@
package eu.kanade.domain.source.service
import tachiyomi.core.preference.getAndSet
class ToggleLanguage(
val preferences: SourcePreferences,
) {
fun await(language: String) {
val isEnabled = language in preferences.enabledLanguages().get()
preferences.enabledLanguages().getAndSet { enabled ->
if (isEnabled) enabled.minus(language) else enabled.plus(language)
}
}
}

View file

@ -1,3 +1,3 @@
package ani.dantotsu.aniyomi package eu.kanade.tachiyomi
typealias PreferenceScreen = androidx.preference.PreferenceScreen typealias PreferenceScreen = androidx.preference.PreferenceScreen

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.animesource package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage

View file

@ -1,10 +1,9 @@
package ani.dantotsu.aniyomi.animesource package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
//import ani.dantotsu.aniyomi.util.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import ani.dantotsu.aniyomi.util.lang.awaitSingle
import rx.Observable import rx.Observable
/** /**

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.animesource package eu.kanade.tachiyomi.animesource
/** /**
* A factory for creating sources at runtime. * A factory for creating sources at runtime.

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.animesource package eu.kanade.tachiyomi.animesource
import ani.dantotsu.aniyomi.animesource.AnimeSource import eu.kanade.tachiyomi.PreferenceScreen
import ani.dantotsu.aniyomi.PreferenceScreen
interface ConfigurableAnimeSource : AnimeSource { interface ConfigurableAnimeSource : AnimeSource {

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.model package eu.kanade.tachiyomi.animesource.model
import ani.dantotsu.aniyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import java.io.Serializable import java.io.Serializable
interface SAnime : Serializable { interface SAnime : Serializable {

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.model package eu.kanade.tachiyomi.animesource.model
import ani.dantotsu.aniyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
class SAnimeImpl : SAnime { class SAnimeImpl : SAnime {

View file

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.animesource.model package eu.kanade.tachiyomi.animesource.model
import android.net.Uri import android.net.Uri
import ani.dantotsu.aniyomi.util.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import okhttp3.Headers import okhttp3.Headers

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.online package eu.kanade.tachiyomi.animesource.online
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.core package eu.kanade.tachiyomi.core
object Constants { object Constants {
const val URL_HELP = "https://aniyomi.org/help/" const val URL_HELP = "https://aniyomi.org/help/"

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.core.preference package eu.kanade.tachiyomi.core.preference
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SharedPreferences.Editor import android.content.SharedPreferences.Editor
@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.preference.Preference
sealed class AndroidPreference<T>( sealed class AndroidPreference<T>(
private val preferences: SharedPreferences, private val preferences: SharedPreferences,

View file

@ -1,18 +1,20 @@
package ani.dantotsu.aniyomi.core.preference package eu.kanade.tachiyomi.core.preference
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.BooleanPrimitive import eu.kanade.tachiyomi.core.preference.AndroidPreference.BooleanPrimitive
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.FloatPrimitive import eu.kanade.tachiyomi.core.preference.AndroidPreference.FloatPrimitive
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.IntPrimitive import eu.kanade.tachiyomi.core.preference.AndroidPreference.IntPrimitive
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.LongPrimitive import eu.kanade.tachiyomi.core.preference.AndroidPreference.LongPrimitive
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.StringPrimitive import eu.kanade.tachiyomi.core.preference.AndroidPreference.Object
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.StringSetPrimitive import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringPrimitive
import ani.dantotsu.aniyomi.core.preference.AndroidPreference.Object import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
class AndroidPreferenceStore( class AndroidPreferenceStore(
context: Context, context: Context,

View file

@ -1,11 +1,11 @@
package ani.dantotsu.aniyomi.data package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import ani.dantotsu.MainActivity import ani.dantotsu.MainActivity
import ani.dantotsu.aniyomi.core.Constants import eu.kanade.tachiyomi.core.Constants
/** /**
* Global [BroadcastReceiver] that runs on UI thread * Global [BroadcastReceiver] that runs on UI thread
* Pending Broadcasts should be made from here. * Pending Broadcasts should be made from here.

View file

@ -1,12 +1,12 @@
package ani.dantotsu.aniyomi.data package eu.kanade.tachiyomi.data.notification
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
import ani.dantotsu.aniyomi.util.system.buildNotificationChannel import eu.kanade.tachiyomi.util.system.buildNotificationChannel
import ani.dantotsu.aniyomi.util.system.buildNotificationChannelGroup import eu.kanade.tachiyomi.util.system.buildNotificationChannelGroup
/** /**
* Class to manage the basic information of all the notifications used in the app. * Class to manage the basic information of all the notifications used in the app.

View file

@ -1,11 +1,11 @@
package ani.dantotsu.aniyomi.util.extension package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.aniyomi.data.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import ani.dantotsu.aniyomi.data.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import ani.dantotsu.aniyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
class ExtensionUpdateNotifier(private val context: Context) { class ExtensionUpdateNotifier(private val context: Context) {

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.util.extension package eu.kanade.tachiyomi.extension
enum class InstallStep { enum class InstallStep {
Idle, Pending, Downloading, Installing, Installed, Error; Idle, Pending, Downloading, Installing, Installed, Error;

View file

@ -1,27 +1,30 @@
package ani.dantotsu.aniyomi.anime package eu.kanade.tachiyomi.extension.anime
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import ani.dantotsu.aniyomi.domain.source.anime.model.AnimeSourceData import eu.kanade.domain.source.service.SourcePreferences
import ani.dantotsu.aniyomi.util.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import ani.dantotsu.aniyomi.util.launchNow import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
import ani.dantotsu.aniyomi.anime.api.AnimeExtensionGithubApi import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstallReceiver import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallReceiver
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstaller import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionLoader import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.util.preference.plusAssign
//import eu.kanade.tachiyomi.util.preference.plusAssign import eu.kanade.tachiyomi.util.system.toast
import ani.dantotsu.aniyomi.util.toast
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import logcat.LogPriority import logcat.LogPriority
import rx.Observable import rx.Observable
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.lang.launchNow
import ani.dantotsu.aniyomi.util.withUIContext import tachiyomi.core.util.lang.withUIContext
import ani.dantotsu.logger import tachiyomi.core.util.system.logcat
import tachiyomi.domain.source.anime.model.AnimeSourceData
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
/** /**
* The manager of anime extensions installed as another apk which extend the available sources. It handles * The manager of anime extensions installed as another apk which extend the available sources. It handles
@ -35,6 +38,7 @@ import ani.dantotsu.logger
*/ */
class AnimeExtensionManager( class AnimeExtensionManager(
private val context: Context, private val context: Context,
private val preferences: SourcePreferences = Injekt.get(),
) { ) {
var isInitialized = false var isInitialized = false
@ -55,7 +59,7 @@ class AnimeExtensionManager(
private val _installedAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Installed>()) private val _installedAnimeExtensionsFlow = MutableStateFlow(emptyList<AnimeExtension.Installed>())
val installedExtensionsFlow = _installedAnimeExtensionsFlow.asStateFlow() val installedExtensionsFlow = _installedAnimeExtensionsFlow.asStateFlow()
private var subLanguagesEnabledOnFirstRun = false private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? { fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedAnimeExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName val pkgName = _installedAnimeExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
@ -92,41 +96,6 @@ class AnimeExtensionManager(
*/ */
private fun initAnimeExtensions() { private fun initAnimeExtensions() {
val animeextensions = AnimeExtensionLoader.loadExtensions(context) val animeextensions = AnimeExtensionLoader.loadExtensions(context)
logcat { "Loaded ${animeextensions.size} anime extensions" }
for (result in animeextensions) {
when (result) {
is AnimeLoadResult.Success -> {
logcat { "Loaded: ${result.extension.pkgName}" }
for(source in result.extension.sources) {
logcat { "Loaded: ${source.name}" }
}
val sc = result.extension.sources.first()
if (sc is AnimeCatalogueSource) {
//val res = sc.fetchSearchAnime(1, "spy x family", AnimeFilterList()).toBlocking().first()
/*val newScope = CoroutineScope(Dispatchers.IO)
newScope.launch {
println("fetching popular anime")
try {
val res = sc.fetchPopularAnime(1).toBlocking().first()
println("res111: $res")
}
catch (e: Exception) {
println("Exception111: $e")
}
}*/
}else{
println("${sc.name} is not AnimeCatalogueSource")
}
}
else -> {
logcat(LogPriority.ERROR) { "Error loading anime extension: $result." }
}
}
}
_installedAnimeExtensionsFlow.value = animeextensions _installedAnimeExtensionsFlow.value = animeextensions
.filterIsInstance<AnimeLoadResult.Success>() .filterIsInstance<AnimeLoadResult.Success>()
@ -147,14 +116,13 @@ class AnimeExtensionManager(
api.findExtensions() api.findExtensions()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { context.toast("Could not update anime extensions") } withUIContext { context.toast("Failed to get extensions list") }
emptyList() emptyList()
} }
enableAdditionalSubLanguages(extensions) enableAdditionalSubLanguages(extensions)
_availableAnimeExtensionsFlow.value = extensions _availableAnimeExtensionsFlow.value = extensions
println("AnimeExtensions: $extensions")
updatedInstalledAnimeExtensionsStatuses(extensions) updatedInstalledAnimeExtensionsStatuses(extensions)
setupAvailableAnimeExtensionsSourcesDataMap(extensions) setupAvailableAnimeExtensionsSourcesDataMap(extensions)
} }
@ -174,7 +142,7 @@ class AnimeExtensionManager(
} }
// Use the source lang as some aren't present on the animeextension level. // Use the source lang as some aren't present on the animeextension level.
/*val availableLanguages = animeextensions val availableLanguages = animeextensions
.flatMap(AnimeExtension.Available::sources) .flatMap(AnimeExtension.Available::sources)
.distinctBy(AvailableAnimeSources::lang) .distinctBy(AvailableAnimeSources::lang)
.map(AvailableAnimeSources::lang) .map(AvailableAnimeSources::lang)
@ -185,7 +153,7 @@ class AnimeExtensionManager(
it != deviceLanguage && it.startsWith(deviceLanguage) it != deviceLanguage && it.startsWith(deviceLanguage)
} }
preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)*/ preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)
subLanguagesEnabledOnFirstRun = true subLanguagesEnabledOnFirstRun = true
} }
@ -196,7 +164,7 @@ class AnimeExtensionManager(
*/ */
private fun updatedInstalledAnimeExtensionsStatuses(availableAnimeExtensions: List<AnimeExtension.Available>) { private fun updatedInstalledAnimeExtensionsStatuses(availableAnimeExtensions: List<AnimeExtension.Available>) {
if (availableAnimeExtensions.isEmpty()) { if (availableAnimeExtensions.isEmpty()) {
//preferences.animeExtensionUpdatesCount().set(0) preferences.animeExtensionUpdatesCount().set(0)
return return
} }
@ -286,7 +254,7 @@ class AnimeExtensionManager(
if (signature !in untrustedSignatures) return if (signature !in untrustedSignatures) return
AnimeExtensionLoader.trustedSignatures += signature AnimeExtensionLoader.trustedSignatures += signature
//preferences.trustedSignatures() += signature preferences.trustedSignatures() += signature
val nowTrustedAnimeExtensions = _untrustedAnimeExtensionsFlow.value.filter { it.signatureHash == signature } val nowTrustedAnimeExtensions = _untrustedAnimeExtensionsFlow.value.filter { it.signatureHash == signature }
_untrustedAnimeExtensionsFlow.value -= nowTrustedAnimeExtensions _untrustedAnimeExtensionsFlow.value -= nowTrustedAnimeExtensions
@ -392,6 +360,6 @@ class AnimeExtensionManager(
} }
private fun updatePendingUpdatesCount() { private fun updatePendingUpdatesCount() {
//preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate }) preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate })
} }
} }

View file

@ -1,13 +1,12 @@
package ani.dantotsu.aniyomi.anime.api package eu.kanade.tachiyomi.extension.anime.api
import android.content.Context import android.content.Context
import ani.dantotsu.aniyomi.util.extension.ExtensionUpdateNotifier import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import ani.dantotsu.aniyomi.anime.model.AvailableAnimeSources import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionLoader import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import ani.dantotsu.aniyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
@ -15,11 +14,13 @@ import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
//import ani.dantotsu.aniyomi.core.preference.Preference import tachiyomi.core.preference.Preference
//import ani.dantotsu.aniyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import ani.dantotsu.aniyomi.util.withIOContext import tachiyomi.core.util.lang.withIOContext
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date
import kotlin.time.Duration.Companion.days
internal class AnimeExtensionGithubApi { internal class AnimeExtensionGithubApi {
@ -28,10 +29,9 @@ internal class AnimeExtensionGithubApi {
private val animeExtensionManager: AnimeExtensionManager by injectLazy() private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val json: Json by injectLazy() private val json: Json by injectLazy()
//private val lastExtCheck: Preference<Long> by lazy { private val lastExtCheck: Preference<Long> by lazy {
// preferenceStore.getLong("last_ext_check", 0) preferenceStore.getLong("last_ext_check", 0)
//} }
private val lastExtCheck: Long = 0
private var requiresFallbackSource = false private var requiresFallbackSource = false
@ -75,14 +75,14 @@ internal class AnimeExtensionGithubApi {
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<AnimeExtension.Installed>? { suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<AnimeExtension.Installed>? {
// Limit checks to once a day at most // Limit checks to once a day at most
//if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) { if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
// return null return null
//} }
val extensions = if (fromAvailableExtensionList) { val extensions = if (fromAvailableExtensionList) {
animeExtensionManager.availableExtensionsFlow.value animeExtensionManager.availableExtensionsFlow.value
} else { } else {
findExtensions().also { }//lastExtCheck.set(Date().time) } findExtensions().also { lastExtCheck.set(Date().time) }
} }
val installedExtensions = AnimeExtensionLoader.loadExtensions(context) val installedExtensions = AnimeExtensionLoader.loadExtensions(context)

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.anime.installer package eu.kanade.tachiyomi.extension.anime.installer
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -8,8 +8,8 @@ import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import ani.dantotsu.aniyomi.util.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Collections import java.util.Collections
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.anime.installer package eu.kanade.tachiyomi.extension.anime.installer
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
@ -8,12 +8,12 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import ani.dantotsu.aniyomi.util.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import ani.dantotsu.aniyomi.util.lang.use import eu.kanade.tachiyomi.util.lang.use
import ani.dantotsu.aniyomi.util.system.getParcelableExtraCompat import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import ani.dantotsu.aniyomi.util.system.getUriSize import eu.kanade.tachiyomi.util.system.getUriSize
import logcat.LogPriority import logcat.LogPriority
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(service) { class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(service) {

View file

@ -1,8 +1,8 @@
package ani.dantotsu.aniyomi.anime.model package eu.kanade.tachiyomi.extension.anime.model
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import ani.dantotsu.aniyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.AnimeSource
import ani.dantotsu.aniyomi.domain.source.anime.model.AnimeSourceData import tachiyomi.domain.source.anime.model.AnimeSourceData
sealed class AnimeExtension { sealed class AnimeExtension {

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.anime.model package eu.kanade.tachiyomi.extension.anime.model
sealed class AnimeLoadResult { sealed class AnimeLoadResult {
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult() class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()

View file

@ -1,12 +1,12 @@
package ani.dantotsu.aniyomi.anime.util package eu.kanade.tachiyomi.extension.anime.util
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import ani.dantotsu.aniyomi.util.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import ani.dantotsu.aniyomi.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.aniyomi.util.system.hasMiuiPackageInstaller import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import ani.dantotsu.aniyomi.util.toast import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds

View file

@ -1,18 +1,18 @@
package ani.dantotsu.aniyomi.anime.util package eu.kanade.tachiyomi.extension.anime.util
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import logcat.LogPriority import logcat.LogPriority
import ani.dantotsu.aniyomi.util.launchNow import tachiyomi.core.util.lang.launchNow
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
/** /**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only * Broadcast receiver that listens for the system's packages installed, updated or removed, and only

View file

@ -1,20 +1,20 @@
package ani.dantotsu.aniyomi.anime.util package eu.kanade.tachiyomi.extension.anime.util
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.IBinder import android.os.IBinder
import eu.kanade.domain.base.BasePreferences
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.aniyomi.domain.base.BasePreferences import eu.kanade.tachiyomi.data.notification.Notifications
import ani.dantotsu.aniyomi.data.Notifications import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
import ani.dantotsu.aniyomi.anime.installer.InstallerAnime import eu.kanade.tachiyomi.extension.anime.installer.PackageInstallerInstallerAnime
import ani.dantotsu.aniyomi.anime.installer.PackageInstallerInstallerAnime import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import ani.dantotsu.aniyomi.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
import ani.dantotsu.aniyomi.util.system.getSerializableExtraCompat import eu.kanade.tachiyomi.util.system.notificationBuilder
import ani.dantotsu.aniyomi.util.system.notificationBuilder
import logcat.LogPriority import logcat.LogPriority
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
class AnimeExtensionInstallService : Service() { class AnimeExtensionInstallService : Service() {

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.anime.util package eu.kanade.tachiyomi.extension.anime.util
import android.app.DownloadManager import android.app.DownloadManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -11,15 +11,15 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.net.toUri import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import ani.dantotsu.aniyomi.util.extension.InstallStep import eu.kanade.domain.base.BasePreferences
import ani.dantotsu.aniyomi.anime.installer.InstallerAnime import eu.kanade.tachiyomi.extension.InstallStep
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
import ani.dantotsu.aniyomi.domain.base.BasePreferences import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import logcat.LogPriority import logcat.LogPriority
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.anime.util package eu.kanade.tachiyomi.extension.anime.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -7,18 +7,18 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import ani.dantotsu.aniyomi.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import ani.dantotsu.aniyomi.animesource.AnimeSource import eu.kanade.tachiyomi.animesource.AnimeSource
import ani.dantotsu.aniyomi.animesource.AnimeSourceFactory import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import ani.dantotsu.aniyomi.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.anime.model.AnimeLoadResult import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import ani.dantotsu.aniyomi.util.lang.Hash import eu.kanade.tachiyomi.util.lang.Hash
import ani.dantotsu.aniyomi.util.system.getApplicationIcon import eu.kanade.tachiyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import ani.dantotsu.aniyomi.util.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**

View file

@ -0,0 +1,365 @@
package eu.kanade.tachiyomi.extension.manga
import android.content.Context
import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import logcat.LogPriority
import rx.Observable
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.source.manga.model.MangaSourceData
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
/**
* The manager of extensions installed as another apk which extend the available sources. It handles
* the retrieval of remotely available extensions as well as installing, updating and removing them.
* To avoid malicious distribution, every extension must be signed and it will only be loaded if its
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
* loaded.
*
* @param context The application context.
* @param preferences The application preferences.
*/
class MangaExtensionManager(
private val context: Context,
private val preferences: SourcePreferences = Injekt.get(),
) {
var isInitialized = false
private set
/**
* API where all the available extensions can be found.
*/
private val api = MangaExtensionGithubApi()
/**
* The installer which installs, updates and uninstalls the extensions.
*/
private val installer by lazy { MangaExtensionInstaller(context) }
private val iconMap = mutableMapOf<String, Drawable>()
private val _installedExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Installed>())
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
}
return null
}
private val _availableExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Available>())
val availableExtensionsFlow = _availableExtensionsFlow.asStateFlow()
private var availableExtensionsSourcesData: Map<Long, MangaSourceData> = emptyMap()
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<MangaExtension.Available>) {
if (extensions.isEmpty()) return
availableExtensionsSourcesData = extensions
.flatMap { ext -> ext.sources.map { it.toSourceData() } }
.associateBy { it.id }
}
fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
private val _untrustedExtensionsFlow = MutableStateFlow(emptyList<MangaExtension.Untrusted>())
val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
init {
initExtensions()
MangaExtensionInstallReceiver(InstallationListener()).register(context)
}
/**
* Loads and registers the installed extensions.
*/
private fun initExtensions() {
val extensions = MangaExtensionLoader.loadMangaExtensions(context)
_installedExtensionsFlow.value = extensions
.filterIsInstance<MangaLoadResult.Success>()
.map { it.extension }
_untrustedExtensionsFlow.value = extensions
.filterIsInstance<MangaLoadResult.Untrusted>()
.map { it.extension }
isInitialized = true
}
/**
* Finds the available extensions in the [api] and updates [availableExtensions].
*/
suspend fun findAvailableExtensions() {
val extensions: List<MangaExtension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast("Failed to get manga extensions") }
emptyList()
}
enableAdditionalSubLanguages(extensions)
_availableExtensionsFlow.value = extensions
updatedInstalledExtensionsStatuses(extensions)
setupAvailableExtensionsSourcesDataMap(extensions)
}
/**
* Enables the additional sub-languages in the app first run. This addresses
* the issue where users still need to enable some specific languages even when
* the device language is inside that major group. As an example, if a user
* has a zh device language, the app will also enable zh-Hans and zh-Hant.
*
* If the user have already changed the enabledLanguages preference value once,
* the new languages will not be added to respect the user enabled choices.
*/
private fun enableAdditionalSubLanguages(extensions: List<MangaExtension.Available>) {
if (subLanguagesEnabledOnFirstRun || extensions.isEmpty()) {
return
}
// Use the source lang as some aren't present on the extension level.
val availableLanguages = extensions
.flatMap(MangaExtension.Available::sources)
.distinctBy(AvailableMangaSources::lang)
.map(AvailableMangaSources::lang)
val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue()
val languagesToEnable = availableLanguages.filter {
it != deviceLanguage && it.startsWith(deviceLanguage)
}
preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)
subLanguagesEnabledOnFirstRun = true
}
/**
* Sets the update field of the installed extensions with the given [availableExtensions].
*
* @param availableExtensions The list of extensions given by the [api].
*/
private fun updatedInstalledExtensionsStatuses(availableExtensions: List<MangaExtension.Available>) {
if (availableExtensions.isEmpty()) {
preferences.mangaExtensionUpdatesCount().set(0)
return
}
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
var changed = false
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
val pkgName = installedExt.pkgName
val availableExt = availableExtensions.find { it.pkgName == pkgName }
if (!installedExt.isUnofficial && availableExt == null && !installedExt.isObsolete) {
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true
} else if (availableExt != null) {
val hasUpdate = installedExt.updateExists(availableExt)
if (installedExt.hasUpdate != hasUpdate) {
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
changed = true
}
}
}
if (changed) {
_installedExtensionsFlow.value = mutInstalledExtensions
}
updatePendingUpdatesCount()
}
/**
* Returns an observable of the installation process for the given extension. It will complete
* once the extension is installed or throws an error. The process will be canceled if
* unsubscribed before its completion.
*
* @param extension The extension to be installed.
*/
fun installExtension(extension: MangaExtension.Available): Observable<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
}
/**
* Returns an observable of the installation process for the given extension. It will complete
* once the extension is updated or throws an error. The process will be canceled if
* unsubscribed before its completion.
*
* @param extension The extension to be updated.
*/
fun updateExtension(extension: MangaExtension.Installed): Observable<InstallStep> {
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
return installExtension(availableExt)
}
fun cancelInstallUpdateExtension(extension: MangaExtension) {
installer.cancelInstall(extension.pkgName)
}
/**
* Sets to "installing" status of an extension installation.
*
* @param downloadId The id of the download.
*/
fun setInstalling(downloadId: Long) {
installer.updateInstallStep(downloadId, InstallStep.Installing)
}
fun updateInstallStep(downloadId: Long, step: InstallStep) {
installer.updateInstallStep(downloadId, step)
}
/**
* Uninstalls the extension that matches the given package name.
*
* @param pkgName The package name of the application to uninstall.
*/
fun uninstallExtension(pkgName: String) {
installer.uninstallApk(pkgName)
}
/**
* Adds the given signature to the list of trusted signatures. It also loads in background the
* extensions that match this signature.
*
* @param signature The signature to whitelist.
*/
fun trustSignature(signature: String) {
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return
MangaExtensionLoader.trustedSignatures += signature
preferences.trustedSignatures() += signature
val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
_untrustedExtensionsFlow.value -= nowTrustedExtensions
val ctx = context
launchNow {
nowTrustedExtensions
.map { extension ->
async { MangaExtensionLoader.loadMangaExtensionFromPkgName(ctx, extension.pkgName) }
}
.map { it.await() }
.forEach { result ->
if (result is MangaLoadResult.Success) {
registerNewExtension(result.extension)
}
}
}
}
/**
* Registers the given extension in this and the source managers.
*
* @param extension The extension to be registered.
*/
private fun registerNewExtension(extension: MangaExtension.Installed) {
_installedExtensionsFlow.value += extension
}
/**
* Registers the given updated extension in this and the source managers previously removing
* the outdated ones.
*
* @param extension The extension to be registered.
*/
private fun registerUpdatedExtension(extension: MangaExtension.Installed) {
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
if (oldExtension != null) {
mutInstalledExtensions -= oldExtension
}
mutInstalledExtensions += extension
_installedExtensionsFlow.value = mutInstalledExtensions
}
/**
* Unregisters the extension in this and the source managers given its package name. Note this
* method is called for every uninstalled application in the system.
*
* @param pkgName The package name of the uninstalled application.
*/
private fun unregisterExtension(pkgName: String) {
val installedExtension = _installedExtensionsFlow.value.find { it.pkgName == pkgName }
if (installedExtension != null) {
_installedExtensionsFlow.value -= installedExtension
}
val untrustedExtension = _untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
if (untrustedExtension != null) {
_untrustedExtensionsFlow.value -= untrustedExtension
}
}
/**
* Listener which receives events of the extensions being installed, updated or removed.
*/
private inner class InstallationListener : MangaExtensionInstallReceiver.Listener {
override fun onExtensionInstalled(extension: MangaExtension.Installed) {
registerNewExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUpdated(extension: MangaExtension.Installed) {
registerUpdatedExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUntrusted(extension: MangaExtension.Untrusted) {
_untrustedExtensionsFlow.value += extension
}
override fun onPackageUninstalled(pkgName: String) {
unregisterExtension(pkgName)
updatePendingUpdatesCount()
}
}
/**
* Extension method to set the update field of an installed extension.
*/
private fun MangaExtension.Installed.withUpdateCheck(): MangaExtension.Installed {
return if (updateExists()) {
copy(hasUpdate = true)
} else {
this
}
}
private fun MangaExtension.Installed.updateExists(availableExtension: MangaExtension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}
private fun updatePendingUpdatesCount() {
preferences.mangaExtensionUpdatesCount().set(_installedExtensionsFlow.value.count { it.hasUpdate })
}
}

View file

@ -0,0 +1,186 @@
package eu.kanade.tachiyomi.extension.manga.api
import android.content.Context
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.util.Date
import kotlin.time.Duration.Companion.days
internal class MangaExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val extensionManager: MangaExtensionManager by injectLazy()
private val json: Json by injectLazy()
private val lastExtCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_ext_check", 0)
}
private var requiresFallbackSource = false
suspend fun findExtensions(): List<MangaExtension.Available> {
return withIOContext {
val githubResponse = if (requiresFallbackSource) {
null
} else {
try {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
requiresFallbackSource = true
null
}
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
.awaitSuccess()
}
val extensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
}
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 100) {
throw Exception()
}
extensions
}
}
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<MangaExtension.Installed>? {
// Limit checks to once a day at most
if (fromAvailableExtensionList && Date().time < lastExtCheck.get() + 1.days.inWholeMilliseconds) {
return null
}
val extensions = if (fromAvailableExtensionList) {
extensionManager.availableExtensionsFlow.value
} else {
findExtensions().also { lastExtCheck.set(Date().time) }
}
val installedExtensions = MangaExtensionLoader.loadMangaExtensions(context)
.filterIsInstance<MangaLoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<MangaExtension.Installed>()
for (installedExt in installedExtensions) {
val pkgName = installedExt.pkgName
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib)
if (hasUpdate) {
extensionsWithUpdate.add(installedExt)
}
}
if (extensionsWithUpdate.isNotEmpty()) {
ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name })
}
return extensionsWithUpdate
}
private fun List<ExtensionJsonObject>.toExtensions(): List<MangaExtension.Available> {
return this
.filter {
val libVersion = it.extractLibVersion()
libVersion >= MangaExtensionLoader.LIB_VERSION_MIN && libVersion <= MangaExtensionLoader.LIB_VERSION_MAX
}
.map {
MangaExtension.Available(
name = it.name.substringAfter("Tachiyomi: "),
pkgName = it.pkg,
versionName = it.version,
versionCode = it.code,
libVersion = it.extractLibVersion(),
lang = it.lang,
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toExtensionSources().orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
)
}
}
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableMangaSources> {
return this.map {
AvailableMangaSources(
id = it.id,
lang = it.lang,
name = it.name,
baseUrl = it.baseUrl,
)
}
}
fun getApkUrl(extension: MangaExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
}
private fun ExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble()
}
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
@Serializable
private data class ExtensionJsonObject(
val name: String,
val pkg: String,
val apk: String,
val lang: String,
val code: Long,
val version: String,
val nsfw: Int,
val hasReadme: Int = 0,
val hasChangelog: Int = 0,
val sources: List<ExtensionSourceJsonObject>?,
)
@Serializable
private data class ExtensionSourceJsonObject(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
)

View file

@ -0,0 +1,170 @@
package eu.kanade.tachiyomi.extension.manga.installer
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import androidx.annotation.CallSuper
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import uy.kohesive.injekt.injectLazy
import java.util.Collections
import java.util.concurrent.atomic.AtomicReference
/**
* Base implementation class for extension installer. To be used inside a foreground [Service].
*/
abstract class InstallerManga(private val service: Service) {
private val extensionManager: MangaExtensionManager by injectLazy()
private var waitingInstall = AtomicReference<Entry>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
cancelQueue(downloadId)
}
}
/**
* Installer readiness. If false, queue check will not run.
*
* @see checkQueue
*/
abstract var ready: Boolean
/**
* Add an item to install queue.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
* @param uri Uri of APK to install
*/
fun addToQueue(downloadId: Long, uri: Uri) {
queue.add(Entry(downloadId, uri))
checkQueue()
}
/**
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
* when the install process for this entry is finished to continue the queue.
*
* @param entry The [Entry] of item to process
* @see continueQueue
*/
@CallSuper
open fun processEntry(entry: Entry) {
extensionManager.setInstalling(entry.downloadId)
}
/**
* Called before queue continues. Override this to handle when the removed entry is
* currently being processed.
*
* @return true if this entry can be removed from queue.
*/
open fun cancelEntry(entry: Entry): Boolean {
return true
}
/**
* Tells the queue to continue processing the next entry and updates the install step
* of the completed entry ([waitingInstall]) to [MangaExtensionManager].
*
* @param resultStep new install step for the processed entry.
* @see waitingInstall
*/
fun continueQueue(resultStep: InstallStep) {
val completedEntry = waitingInstall.getAndSet(null)
if (completedEntry != null) {
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
checkQueue()
}
}
/**
* Checks the queue. The provided service will be stopped if the queue is empty.
* Will not be run when not ready.
*
* @see ready
*/
fun checkQueue() {
if (!ready) {
return
}
if (queue.isEmpty()) {
service.stopSelf()
return
}
val nextEntry = queue.first()
if (waitingInstall.compareAndSet(null, nextEntry)) {
queue.removeFirst()
processEntry(nextEntry)
}
}
/**
* Call this method when the provided service is destroyed.
*/
@CallSuper
open fun onDestroy() {
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
queue.clear()
waitingInstall.set(null)
}
protected fun getActiveEntry(): Entry? = waitingInstall.get()
/**
* Cancels queue for the provided download ID if exists.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
*/
private fun cancelQueue(downloadId: Long) {
val waitingInstall = this.waitingInstall.get()
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
if (cancelEntry(toCancel)) {
queue.remove(toCancel)
if (waitingInstall == toCancel) {
// Currently processing removed entry, continue queue
this.waitingInstall.set(null)
checkQueue()
}
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
}
}
/**
* Install item to queue.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
* @param uri Uri of APK to install
*/
data class Entry(val downloadId: Long, val uri: Uri)
init {
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
}
companion object {
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
/**
* Attempts to cancel the installation entry for the provided download ID.
*
* @param downloadId Download ID as known by [MangaExtensionManager]
*/
fun cancelInstallQueue(context: Context, downloadId: Long) {
val intent = Intent(ACTION_CANCEL_QUEUE)
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
}
}

View file

@ -0,0 +1,107 @@
package eu.kanade.tachiyomi.extension.manga.installer
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.os.Build
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.util.lang.use
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.getUriSize
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
class PackageInstallerInstallerManga(private val service: Service) : InstallerManga(service) {
private val packageInstaller = service.packageManager.packageInstaller
private val packageActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
if (userAction == null) {
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
continueQueue(InstallStep.Error)
return
}
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(userAction)
}
PackageInstaller.STATUS_FAILURE_ABORTED -> {
continueQueue(InstallStep.Idle)
}
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
else -> continueQueue(InstallStep.Error)
}
}
}
private var activeSession: Pair<Entry, Int>? = null
// Always ready
override var ready = true
override fun processEntry(entry: Entry) {
super.processEntry(entry)
activeSession = null
try {
val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
activeSession = entry to packageInstaller.createSession(installParams)
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
installParams.setSize(fileSize)
val inputStream = service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
val session = packageInstaller.openSession(activeSession!!.second)
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
session.use {
arrayOf(inputStream, outputStream).use {
inputStream.copyTo(outputStream)
session.fsync(outputStream)
}
val intentSender = PendingIntent.getBroadcast(
service,
activeSession!!.second,
Intent(INSTALL_ACTION),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
).intentSender
session.commit(intentSender)
}
} catch (e: Exception) {
logcat(LogPriority.ERROR) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
activeSession?.let { (_, sessionId) ->
packageInstaller.abandonSession(sessionId)
}
continueQueue(InstallStep.Error)
}
}
override fun cancelEntry(entry: Entry): Boolean {
activeSession?.let { (activeEntry, sessionId) ->
if (activeEntry == entry) {
packageInstaller.abandonSession(sessionId)
return false
}
}
return true
}
override fun onDestroy() {
service.unregisterReceiver(packageActionReceiver)
super.onDestroy()
}
init {
service.registerReceiver(packageActionReceiver, IntentFilter(INSTALL_ACTION))
}
}
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"

View file

@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.extension.manga.model
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.source.MangaSource
import tachiyomi.domain.source.manga.model.MangaSourceData
sealed class MangaExtension {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract val libVersion: Double
abstract val lang: String?
abstract val isNsfw: Boolean
abstract val hasReadme: Boolean
abstract val hasChangelog: Boolean
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val pkgFactory: String?,
val sources: List<MangaSource>,
val icon: Drawable?,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
) : MangaExtension()
data class Available(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val sources: List<AvailableMangaSources>,
val apkName: String,
val iconUrl: String,
) : MangaExtension()
data class Untrusted(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false,
override val hasReadme: Boolean = false,
override val hasChangelog: Boolean = false,
) : MangaExtension()
}
data class AvailableMangaSources(
val id: Long,
val lang: String,
val name: String,
val baseUrl: String,
) {
fun toSourceData(): MangaSourceData {
return MangaSourceData(
id = this.id,
lang = this.lang,
name = this.name,
)
}
}

View file

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.extension.manga.model
sealed class MangaLoadResult {
class Success(val extension: MangaExtension.Installed) : MangaLoadResult()
class Untrusted(val extension: MangaExtension.Untrusted) : MangaLoadResult()
object Error : MangaLoadResult()
}

View file

@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
/**
* Activity used to install extensions, because we can only receive the result of the installation
* with [startActivityForResult], which we need to update the UI.
*/
class MangaExtensionInstallActivity : Activity() {
// MIUI package installer bug workaround
private var ignoreUntil = 0L
private var ignoreResult = false
private var hasIgnoredResult = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(intent.data, intent.type)
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (hasMiuiPackageInstaller) {
ignoreResult = true
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
}
try {
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
} catch (error: Exception) {
// Either install package can't be found (probably bots) or there's a security exception
// with the download manager. Nothing we can workaround.
toast(error.message)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (ignoreResult && System.nanoTime() < ignoreUntil) {
hasIgnoredResult = true
return
}
if (requestCode == INSTALL_REQUEST_CODE) {
checkInstallationResult(resultCode)
}
finish()
}
override fun onStart() {
super.onStart()
if (hasIgnoredResult) {
checkInstallationResult(RESULT_CANCELED)
finish()
}
}
private fun checkInstallationResult(resultCode: Int) {
val downloadId = intent.extras!!.getLong(MangaExtensionInstaller.EXTRA_DOWNLOAD_ID)
val extensionManager = Injekt.get<MangaExtensionManager>()
val newStep = when (resultCode) {
RESULT_OK -> InstallStep.Installed
RESULT_CANCELED -> InstallStep.Idle
else -> InstallStep.Error
}
extensionManager.updateInstallStep(downloadId, newStep)
}
}
private const val INSTALL_REQUEST_CODE = 500

View file

@ -0,0 +1,130 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import logcat.LogPriority
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.system.logcat
/**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
* notifies the given [listener] when the package is an extension.
*
* @param listener The listener that should be notified of extension installation events.
*/
internal class MangaExtensionInstallReceiver(private val listener: Listener) :
BroadcastReceiver() {
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
context.registerReceiver(this, filter)
}
/**
* Returns the intent filter this receiver should subscribe to.
*/
private val filter
get() = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REPLACED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
/**
* Called when one of the events of the [filter] is received. When the package is an extension,
* it's loaded in background and it notifies the [listener] when finished.
*/
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (isReplacing(intent)) return
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is MangaLoadResult.Success -> listener.onExtensionInstalled(result.extension)
is MangaLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
when (val result = getExtensionFromIntent(context, intent)) {
is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
// Not needed as a package can't be upgraded if the signature is different
// is LoadResult.Untrusted -> {}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
if (isReplacing(intent)) return
val pkgName = getPackageNameFromIntent(intent)
if (pkgName != null) {
listener.onPackageUninstalled(pkgName)
}
}
}
}
/**
* Returns true if this package is performing an update.
*
* @param intent The intent that triggered the event.
*/
private fun isReplacing(intent: Intent): Boolean {
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
}
/**
* Returns the extension triggered by the given intent.
*
* @param context The application context.
* @param intent The intent containing the package name of the extension.
*/
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
val pkgName = getPackageNameFromIntent(intent)
if (pkgName == null) {
logcat(LogPriority.WARN) { "Package name not found" }
return MangaLoadResult.Error
}
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
MangaExtensionLoader.loadMangaExtensionFromPkgName(
context,
pkgName,
)
}.await()
}
/**
* Returns the package name of the installed, updated or removed application.
*/
private fun getPackageNameFromIntent(intent: Intent?): String? {
return intent?.data?.encodedSchemeSpecificPart ?: return null
}
/**
* Listener that receives extension installation events.
*/
interface Listener {
fun onExtensionInstalled(extension: MangaExtension.Installed)
fun onExtensionUpdated(extension: MangaExtension.Installed)
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
fun onPackageUninstalled(pkgName: String)
}
}

View file

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import ani.dantotsu.R
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
import eu.kanade.tachiyomi.extension.manga.installer.PackageInstallerInstallerManga
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
import eu.kanade.tachiyomi.util.system.notificationBuilder
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
class MangaExtensionInstallService : Service() {
private var installer: InstallerManga? = null
override fun onCreate() {
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
setSmallIcon(R.drawable.spinner_icon)
setAutoCancel(false)
setOngoing(true)
setShowWhen(false)
setContentTitle("Installing manga extension...")
setProgress(100, 100, true)
}.build()
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.data
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
EXTRA_INSTALLER,
)
if (uri == null || id == null || installerUsed == null) {
stopSelf()
return START_NOT_STICKY
}
if (installer == null) {
installer = when (installerUsed) {
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerManga(this)
else -> {
logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" }
stopSelf()
return START_NOT_STICKY
}
}
}
installer!!.addToQueue(id, uri)
return START_NOT_STICKY
}
override fun onDestroy() {
installer?.onDestroy()
installer = null
}
override fun onBind(i: Intent?): IBinder? = null
companion object {
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
fun getIntent(
context: Context,
downloadId: Long,
uri: Uri,
installer: BasePreferences.ExtensionInstaller,
): Intent {
return Intent(context, MangaExtensionInstallService::class.java)
.setDataAndType(uri, MangaExtensionInstaller.APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.putExtra(EXTRA_INSTALLER, installer)
}
}
}

View file

@ -0,0 +1,266 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.util.storage.getUriCompat
import logcat.LogPriority
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.TimeUnit
/**
* The installer which installs, updates and uninstalls the extensions.
*
* @param context The application context.
*/
internal class MangaExtensionInstaller(private val context: Context) {
/**
* The system's download manager
*/
private val downloadManager = context.getSystemService<DownloadManager>()!!
/**
* The broadcast receiver which listens to download completion events.
*/
private val downloadReceiver = DownloadCompletionReceiver()
/**
* The currently requested downloads, with the package name (unique id) as key, and the id
* returned by the download manager.
*/
private val activeDownloads = hashMapOf<String, Long>()
/**
* Relay used to notify the installation step of every download.
*/
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
/**
* Adds the given extension to the downloads queue and returns an observable containing its
* step in the installation process.
*
* @param url The url of the apk.
* @param extension The extension to install.
*/
fun downloadAndInstall(url: String, extension: MangaExtension) = Observable.defer {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
if (oldDownload != null) {
deleteDownload(pkgName)
}
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
downloadsRelay.filter { it.first == id }
.map { it.second }
// Poll download status
.mergeWith(pollStatus(id))
// Stop when the application is installed or errors
.takeUntil { it.isCompleted() }
// Always notify on main thread
.observeOn(AndroidSchedulers.mainThread())
// Always remove the download when unsubscribed
.doOnUnsubscribe { deleteDownload(pkgName) }
}
/**
* Returns an observable that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
private fun pollStatus(id: Long): Observable<InstallStep> {
val query = DownloadManager.Query().setFilterById(id)
return Observable.interval(0, 1, TimeUnit.SECONDS)
// Get the current download status
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
}
// Ignore duplicate results
.distinctUntilChanged()
// Stop polling when the download fails or finishes
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
// Map to our model
.flatMap { status ->
when (status) {
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
else -> Observable.empty()
}
}
}
/**
* Starts an intent to install the extension at the given uri.
*
* @param uri The uri of the extension to install.
*/
fun installApk(downloadId: Long, uri: Uri) {
when (val installer = extensionInstaller.get()) {
BasePreferences.ExtensionInstaller.LEGACY -> {
val intent = Intent(context, MangaExtensionInstallActivity::class.java)
.setDataAndType(uri, APK_MIME)
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
}
else -> {
val intent =
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
ContextCompat.startForegroundService(context, intent)
}
}
}
/**
* Cancels extension install and remove from download manager and installer.
*/
fun cancelInstall(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName) ?: return
downloadManager.remove(downloadId)
InstallerManga.cancelInstallQueue(context, downloadId)
}
/**
* Starts an intent to uninstall the extension by the given package name.
*
* @param pkgName The package name of the extension to uninstall
*/
fun uninstallApk(pkgName: String) {
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
/**
* Sets the step of the installation of an extension.
*
* @param downloadId The id of the download.
* @param step New install step.
*/
fun updateInstallStep(downloadId: Long, step: InstallStep) {
downloadsRelay.call(downloadId to step)
}
/**
* Deletes the download for the given package name.
*
* @param pkgName The package name of the download to delete.
*/
private fun deleteDownload(pkgName: String) {
val downloadId = activeDownloads.remove(pkgName)
if (downloadId != null) {
downloadManager.remove(downloadId)
}
if (activeDownloads.isEmpty()) {
downloadReceiver.unregister()
}
}
/**
* Receiver that listens to download status events.
*/
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
/**
* Whether this receiver is currently registered.
*/
private var isRegistered = false
/**
* Registers this receiver if it's not already.
*/
fun register() {
if (isRegistered) return
isRegistered = true
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
context.registerReceiver(this, filter)
}
/**
* Unregisters this receiver if it's not already.
*/
fun unregister() {
if (!isRegistered) return
isRegistered = false
context.unregisterReceiver(this)
}
/**
* Called when a download event is received. It looks for the download in the current active
* downloads and notifies its installation step.
*/
override fun onReceive(context: Context, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
// Avoid events for downloads we didn't request
if (id !in activeDownloads.values) return
val uri = downloadManager.getUriForDownloadedFile(id)
// Set next installation step
if (uri == null) {
logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" }
downloadsRelay.call(id to InstallStep.Error)
return
}
val query = DownloadManager.Query().setFilterById(id)
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val localUri = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context))
}
}
}
}
companion object {
const val APK_MIME = "application/vnd.android.package-archive"
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
const val FILE_SCHEME = "file://"
}
}

View file

@ -0,0 +1,232 @@
package eu.kanade.tachiyomi.extension.manga.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
@SuppressLint("PackageManagerGetSignatures")
internal object MangaExtensionLoader {
private val preferences: SourcePreferences by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.5
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
val pkgManager = context.packageManager
@Suppress("DEPRECATION")
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion
return runBlocking {
val deferred = extPkgs.map {
async { loadMangaExtension(context, it.packageName, it) }
}
deferred.map { it.await() }
}
}
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
val pkgInfo = try {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
logcat(LogPriority.ERROR, error)
return MangaLoadResult.Error
}
if (!isPackageAnExtension(pkgInfo)) {
logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
return MangaLoadResult.Error
}
return loadMangaExtension(context, pkgName, pkgInfo)
}
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
private fun loadMangaExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): MangaLoadResult {
val pkgManager = context.packageManager
val appInfo = try {
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
logcat(LogPriority.ERROR, error)
return MangaLoadResult.Error
}
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Missing versionName for extension $extName" }
return MangaLoadResult.Error
}
// Validate lib version
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
logcat(LogPriority.WARN) {
"Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
}
return MangaLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
if (signatureHash == null) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return MangaLoadResult.Error
} else if (signatureHash !in trustedSignatures) {
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
return MangaLoadResult.Untrusted(extension)
}
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) {
logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" }
return MangaLoadResult.Error
}
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
.split(";")
.map {
val sourceClass = it.trim()
if (sourceClass.startsWith(".")) {
pkgInfo.packageName + sourceClass
} else {
sourceClass
}
}
.flatMap {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is MangaSource -> listOf(obj)
is SourceFactory -> obj.createSources()
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
return MangaLoadResult.Error
}
}
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extension = MangaExtension.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
hasReadme = hasReadme,
hasChangelog = hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature,
icon = context.getApplicationIcon(pkgName),
)
return MangaLoadResult.Success(extension)
}
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
}
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
}

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.util.network package eu.kanade.tachiyomi.network
import android.webkit.CookieManager import android.webkit.CookieManager
import okhttp3.Cookie import okhttp3.Cookie

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.util.network package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient

View file

@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import ani.dantotsu.aniyomi.util.network.AndroidCookieJar import eu.kanade.tachiyomi.network.AndroidCookieJar
import ani.dantotsu.aniyomi.util.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import ani.dantotsu.aniyomi.util.network.PREF_DOH_GOOGLE import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
import ani.dantotsu.aniyomi.util.network.dohCloudflare import eu.kanade.tachiyomi.network.dohCloudflare
import ani.dantotsu.aniyomi.util.network.dohGoogle import eu.kanade.tachiyomi.network.dohGoogle
import ani.dantotsu.aniyomi.util.network.interceptor.CloudflareInterceptor import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache import okhttp3.Cache
@ -75,5 +75,5 @@ class NetworkHelper(
.build() .build()
} }
fun defaultUserAgentProvider() = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0"//preferences.defaultUserAgent().get().trim() fun defaultUserAgentProvider() = "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"//preferences.defaultUserAgent().get().trim()
} }

View file

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import ani.dantotsu.aniyomi.util.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import ani.dantotsu.aniyomi.util.network.ProgressResponseBody import eu.kanade.tachiyomi.network.ProgressResponseBody
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.util.network package eu.kanade.tachiyomi.network
interface ProgressListener { interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean) fun update(bytesRead: Long, contentLength: Long, done: Boolean)

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.util.network package eu.kanade.tachiyomi.network
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.ResponseBody import okhttp3.ResponseBody

View file

@ -18,7 +18,9 @@ fun GET(
headers: Headers = DEFAULT_HEADERS, headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL, cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request { ): Request {
return GET(url.toHttpUrl(), headers, cache) val nUrl = url.toHttpUrl()
val g = GET(nUrl, headers, cache)
return g
} }
/** /**

View file

@ -1,14 +1,14 @@
package ani.dantotsu.aniyomi.util.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.aniyomi.util.network.AndroidCookieJar import eu.kanade.tachiyomi.network.AndroidCookieJar
import ani.dantotsu.aniyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import ani.dantotsu.aniyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import ani.dantotsu.aniyomi.util.toast import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor

View file

@ -0,0 +1,105 @@
package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.util.ArrayDeque
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor that handles rate limiting.
*
* Examples:
*
* permits = 5, period = 1, unit = seconds => 5 requests per second
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.3
*
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(null, permits, period, unit))
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor(
private val host: String?,
private val permits: Int,
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
private val fairLock = Semaphore(1, true)
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
if (call.isCanceled()) throw IOException("Canceled")
val request = chain.request()
when (host) {
null, request.url.host -> {} // need rate limit
else -> return chain.proceed(request)
}
try {
fairLock.acquire()
} catch (e: InterruptedException) {
throw IOException(e)
}
val requestQueue = this.requestQueue
val timestamp: Long
try {
synchronized(requestQueue) {
while (requestQueue.size >= permits) { // queue is full, remove expired entries
val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis
var hasRemovedExpired = false
while (requestQueue.isEmpty().not() && requestQueue.first <= periodStart) {
requestQueue.removeFirst()
hasRemovedExpired = true
}
if (call.isCanceled()) {
throw IOException("Canceled")
} else if (hasRemovedExpired) {
break
} else {
try { // wait for the first entry to expire, or notified by cached response
(requestQueue as Object).wait(requestQueue.first - periodStart)
} catch (_: InterruptedException) {
continue
}
}
}
// add request to queue
timestamp = SystemClock.elapsedRealtime()
requestQueue.addLast(timestamp)
}
} finally {
fairLock.release()
}
val response = chain.proceed(request)
if (response.networkResponse == null) { // response is cached, remove it from queue
synchronized(requestQueue) {
if (requestQueue.isEmpty() || timestamp < requestQueue.first) return@synchronized
requestQueue.removeFirstOccurrence(timestamp)
(requestQueue as Object).notifyAll()
}
}
return response
}
}

View file

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.3
*
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period, unit))

View file

@ -1,20 +1,20 @@
package ani.dantotsu.aniyomi.util.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import ani.dantotsu.aniyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import ani.dantotsu.aniyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import ani.dantotsu.aniyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import ani.dantotsu.aniyomi.util.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import ani.dantotsu.aniyomi.util.launchUI import tachiyomi.core.util.lang.launchUI
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit

View file

@ -1,7 +1,7 @@
package ani.dantotsu.aniyomi.source package eu.kanade.tachiyomi.source
import ani.dantotsu.aniyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import ani.dantotsu.aniyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import rx.Observable import rx.Observable
interface CatalogueSource : MangaSource { interface CatalogueSource : MangaSource {

View file

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source
import eu.kanade.tachiyomi.PreferenceScreen
interface ConfigurableSource : MangaSource {
fun setupPreferenceScreen(screen: PreferenceScreen)
}

View file

@ -1,9 +1,9 @@
package ani.dantotsu.aniyomi.source package eu.kanade.tachiyomi.source
import ani.dantotsu.aniyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import ani.dantotsu.aniyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import ani.dantotsu.aniyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ani.dantotsu.aniyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import rx.Observable import rx.Observable
/** /**

View file

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.source
/**
* A factory for creating sources at runtime.
*/
interface SourceFactory {
/**
* Create a new copy of the sources
* @return The created sources
*/
fun createSources(): List<MangaSource>
}

View file

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source
/**
* A source that explicitly doesn't require traffic considerations.
*
* This typically applies for self-hosted sources.
*/
interface UnmeteredSource

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
sealed class Filter<T>(val name: String, var state: T) { sealed class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0) open class Header(name: String) : Filter<Any>(name, 0)

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list { data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {

View file

@ -1,3 +1,3 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean) data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

View file

@ -1,19 +1,17 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
import android.net.Uri import android.net.Uri
import ani.dantotsu.aniyomi.util.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.Serializable import java.io.Serializable
import kotlinx.serialization.Transient
@Serializable
open class Page( open class Page(
val index: Int, val index: Int,
val url: String = "", val url: String = "",
var imageUrl: String? = null, var imageUrl: String? = null,
@Transient var uri: Uri? = null, // Deprecated but can't be deleted due to extensions @Transient var uri: Uri? = null, // Deprecated but can't be deleted due to extensions
) : ProgressListener { ) : Serializable, ProgressListener {
val number: Int val number: Int
get() = index + 1 get() = index + 1

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
import java.io.Serializable import java.io.Serializable

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
class SChapterImpl : SChapter { class SChapterImpl : SChapter {

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
import java.io.Serializable import java.io.Serializable

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
class SMangaImpl : SManga { class SMangaImpl : SManga {

View file

@ -1,4 +1,4 @@
package ani.dantotsu.aniyomi.source.model package eu.kanade.tachiyomi.source.model
/** /**
* Define the update strategy for a single [SManga]. * Define the update strategy for a single [SManga].

View file

@ -5,12 +5,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import ani.dantotsu.aniyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import ani.dantotsu.aniyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import ani.dantotsu.aniyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import ani.dantotsu.aniyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import ani.dantotsu.aniyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import ani.dantotsu.aniyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request

View file

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
}
private fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
return Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) }
}
private fun HttpSource.getImageUrl(page: Page): Observable<Page> {
page.status = Page.State.LOAD_PAGE
return fetchImageUrl(page)
.doOnError { page.status = Page.State.ERROR }
.onErrorReturn { null }
.doOnNext { page.imageUrl = it }
.map { page }
}

View file

@ -0,0 +1,201 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*/
@Suppress("unused")
abstract class ParsedHttpSource : HttpSource() {
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
protected abstract fun popularMangaSelector(): String
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [popularMangaSelector].
*/
protected abstract fun popularMangaFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun popularMangaNextPageSelector(): String?
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
protected abstract fun searchMangaSelector(): String
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchMangaSelector].
*/
protected abstract fun searchMangaFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun searchMangaNextPageSelector(): String?
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
protected abstract fun latestUpdatesSelector(): String
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [latestUpdatesSelector].
*/
protected abstract fun latestUpdatesFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun latestUpdatesNextPageSelector(): String?
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup())
}
/**
* Returns the details of the manga from the given [document].
*
* @param document the parsed document.
*/
protected abstract fun mangaDetailsParse(document: Document): SManga
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) }
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
*/
protected abstract fun chapterListSelector(): String
/**
* Returns a chapter from the given element.
*
* @param element an element obtained from [chapterListSelector].
*/
protected abstract fun chapterFromElement(element: Element): SChapter
/**
* Parses the response from the site and returns the page list.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
return pageListParse(response.asJsoup())
}
/**
* Returns a page list from the given document.
*
* @param document the parsed document.
*/
protected abstract fun pageListParse(document: Document): List<Page>
/**
* Parse the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup())
}
/**
* Returns the absolute url to the source image from the document.
*
* @param document the parsed document.
*/
protected abstract fun imageUrlParse(document: Document): String
}

View file

@ -1,3 +1,3 @@
package ani.dantotsu.aniyomi.util package eu.kanade.tachiyomi.util
//expect suspend fun <T> Observable<T>.awaitSingle(): T //expect suspend fun <T> Observable<T>.awaitSingle(): T

Some files were not shown because too many files have changed in this diff Show more