This commit is contained in:
Finnley Somdahl 2023-12-01 01:22:15 -06:00
parent 1df528c0dc
commit afa960c808
171 changed files with 3458 additions and 1915 deletions

View file

@ -1,9 +1,8 @@
package ani.dantotsu.parsers
import android.net.Uri
import ani.dantotsu.R
import ani.dantotsu.FileUrl
import eu.kanade.tachiyomi.animesource.model.SEpisode
import ani.dantotsu.R
import ani.dantotsu.asyncMap
import ani.dantotsu.currContext
import ani.dantotsu.loadData
@ -11,6 +10,7 @@ import ani.dantotsu.others.MalSyncBackup
import ani.dantotsu.saveData
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlin.properties.Delegates
/**
@ -23,7 +23,11 @@ abstract class AnimeParser : BaseParser() {
/**
* Takes ShowResponse.link & ShowResponse.extra (if you added any) as arguments & gives a list of total episodes present on the site.
* **/
abstract suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode>
abstract suspend fun loadEpisodes(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode>
/**
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Episode Number known by app as arguments
@ -31,10 +35,15 @@ abstract class AnimeParser : BaseParser() {
* Returns the latest episode (If overriding, Make sure the episode is actually the latest episode)
* Returns null, if no latest episode is found.
* **/
open suspend fun getLatestEpisode(animeLink: String, extra: Map<String, String>?, sAnime: SAnime, latest: Float): Episode?{
open suspend fun getLatestEpisode(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime,
latest: Float
): Episode? {
val episodes = loadEpisodes(animeLink, extra, sAnime)
val max = episodes
.maxByOrNull { it.number.toFloatOrNull()?:0f }
.maxByOrNull { it.number.toFloatOrNull() ?: 0f }
return max
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
}
@ -44,7 +53,11 @@ abstract class AnimeParser : BaseParser() {
*
* This returns a Map of "Video Server's Name" & "Link/Data" of all the Video Servers present on the site, which can be further used by loadVideoServers() & loadSingleVideoServer()
* **/
abstract suspend fun loadVideoServers(episodeLink: String, extra: Map<String,String>?, sEpisode: SEpisode): List<VideoServer>
abstract suspend fun loadVideoServers(
episodeLink: String,
extra: Map<String, String>?,
sEpisode: SEpisode
): List<VideoServer>
/**
@ -75,10 +88,12 @@ abstract class AnimeParser : BaseParser() {
* **/
open suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
var domain = Uri.parse(server.embed.url).host ?: return null
if (domain.startsWith("www.")) {domain = domain.substring(4)}
if (domain.startsWith("www.")) {
domain = domain.substring(4)
}
val extractor: VideoExtractor? = when (domain) {
else -> {
else -> {
println("$name : No extractor found for: $domain | ${server.embed.url}")
null
}
@ -98,7 +113,12 @@ abstract class AnimeParser : BaseParser() {
*
* Doesn't need to be overridden, if the parser is following the norm.
* **/
open suspend fun loadByVideoServers(episodeUrl: String, extra: Map<String,String>?, sEpisode: SEpisode, callback: (VideoExtractor) -> Unit) {
open suspend fun loadByVideoServers(
episodeUrl: String,
extra: Map<String, String>?,
sEpisode: SEpisode,
callback: (VideoExtractor) -> Unit
) {
tryWithSuspend(true) {
loadVideoServers(episodeUrl, extra, sEpisode).asyncMap {
getVideoExtractor(it)?.apply {
@ -116,7 +136,13 @@ abstract class AnimeParser : BaseParser() {
*
* Doesn't need to be overridden, if the parser is following the norm.
* **/
open suspend fun loadSingleVideoServer(serverName: String, episodeUrl: String, extra: Map<String,String>?, sEpisode: SEpisode, post: Boolean): VideoExtractor? {
open suspend fun loadSingleVideoServer(
serverName: String,
episodeUrl: String,
extra: Map<String, String>?,
sEpisode: SEpisode,
post: Boolean
): VideoExtractor? {
return tryWithSuspend(post) {
loadVideoServers(episodeUrl, extra, sEpisode).apply {
find { it.name == serverName }?.also {
@ -159,27 +185,43 @@ abstract class AnimeParser : BaseParser() {
val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else ""
var loaded = loadData<ShowResponse>("${saveName}${dub}_$mediaId")
if (loaded == null && malSyncBackupName.isNotEmpty())
loaded = MalSyncBackup.get(mediaId, malSyncBackupName, selectDub)?.also { saveShowResponse(mediaId, it, true) }
loaded = MalSyncBackup.get(mediaId, malSyncBackupName, selectDub)
?.also { saveShowResponse(mediaId, it, true) }
return loaded
}
override fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean) {
if (response != null) {
checkIfVariablesAreEmpty()
setUserText("${if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(R.string.found)} : ${response.name}")
setUserText(
"${
if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(
R.string.found
)
} : ${response.name}"
)
val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else ""
saveData("${saveName}${dub}_$mediaId", response)
}
}
}
class EmptyAnimeParser: AnimeParser() {
class EmptyAnimeParser : AnimeParser() {
override val name: String = "None"
override val saveName: String = "None"
override val isDubAvailableSeparately: Boolean = false
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> = emptyList()
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> = emptyList()
override suspend fun loadEpisodes(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> = emptyList()
override suspend fun loadVideoServers(
episodeLink: String,
extra: Map<String, String>?,
sEpisode: SEpisode
): List<VideoServer> = emptyList()
override suspend fun search(query: String): List<ShowResponse> = emptyList()
}
@ -209,7 +251,7 @@ data class Episode(
/**
* In case, you want to pass extra data
* **/
val extra: Map<String,String>? = null,
val extra: Map<String, String>? = null,
//SEpisode from Aniyomi
val sEpisode: SEpisode? = null
@ -221,7 +263,7 @@ data class Episode(
thumbnail: String,
description: String? = null,
isFiller: Boolean = false,
extra: Map<String,String>? = null
extra: Map<String, String>? = null
) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra)
constructor(
@ -231,7 +273,7 @@ data class Episode(
thumbnail: String,
description: String? = null,
isFiller: Boolean = false,
extra: Map<String,String>? = null,
extra: Map<String, String>? = null,
sEpisode: SEpisode? = null
) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra, sEpisode)
}

View file

@ -1,8 +1,8 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import ani.dantotsu.lazyList
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
@ -30,8 +30,8 @@ object AnimeSources : WatchSources() {
object HAnimeSources : WatchSources() {
private val aList: List<Lazier<BaseParser>> = lazyList(
private val aList: List<Lazier<BaseParser>> = lazyList(
)
override val list = listOf(aList,AnimeSources.list).flatten()
override val list = listOf(aList, AnimeSources.list).flatten()
}

View file

@ -3,7 +3,6 @@ package ani.dantotsu.parsers
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
@ -12,21 +11,21 @@ import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import ani.dantotsu.FileUrl
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
@ -41,13 +40,13 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.UnsupportedEncodingException
import java.net.URL
import java.net.URLDecoder
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.*
import java.io.UnsupportedEncodingException
import java.util.regex.Pattern
class AniyomiAdapter {
@ -107,7 +106,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} else {
var episodeCounter = 1f
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
val seasonGroups = res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
val seasonGroups =
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
seasonGroups.keys.sorted().flatMap { season ->
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
@ -302,7 +302,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
return ret
}
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData>{
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData> {
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
@ -348,7 +348,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
println("Response: ${response.message}")
// Convert the Response to an InputStream
val inputStream = response.body?.byteStream()
val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
@ -379,7 +379,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val response = httpSource.getImage(page)
// Convert the Response to an InputStream
val inputStream = response.body?.byteStream()
val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
@ -578,7 +578,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
}
}
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video): ani.dantotsu.parsers.Video {
private fun AniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
// Find the number value from the .quality string
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
@ -608,14 +608,15 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
// If the format is still undetermined, log an error or handle it appropriately
if (format == null) {
logger("Unknown video format: $videoUrl")
FirebaseCrashlytics.getInstance().recordException(Exception("Unknown video format: $videoUrl"))
FirebaseCrashlytics.getInstance()
.recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER
}
val headersMap: Map<String, String> =
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
return ani.dantotsu.parsers.Video(
return Video(
number,
format,
FileUrl(videoUrl, headersMap),
@ -647,7 +648,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
private fun findSubtitleType(url: String): SubtitleType? {
// First, try to determine the type based on the URL file extension
val type: SubtitleType? = when {
val type: SubtitleType = when {
url.endsWith(".vtt", true) -> SubtitleType.VTT
url.endsWith(".ass", true) -> SubtitleType.ASS
url.endsWith(".srt", true) -> SubtitleType.SRT

View file

@ -1,13 +1,18 @@
package ani.dantotsu.parsers
import ani.dantotsu.*
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.loadData
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.saveData
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.Serializable
import java.net.URLDecoder
import java.net.URLEncoder
import me.xdrop.fuzzywuzzy.FuzzySearch
abstract class BaseParser {
@ -62,18 +67,32 @@ abstract class BaseParser {
logger("Result: ${it.name}")
}
val sortedResults = if (results.isNotEmpty()) {
results.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.mainName().lowercase()) }
results.sortedByDescending {
FuzzySearch.ratio(
it.name.lowercase(),
mediaObj.mainName().lowercase()
)
}
} else {
emptyList()
}
response = sortedResults.firstOrNull()
if (response == null || FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase()) < 100) {
if (response == null || FuzzySearch.ratio(
response.name.lowercase(),
mediaObj.mainName().lowercase()
) < 100
) {
setUserText("Searching : ${mediaObj.nameRomaji}")
logger("Searching : ${mediaObj.nameRomaji}")
val romajiResults = search(mediaObj.nameRomaji)
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
romajiResults.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.nameRomaji.lowercase()) }
romajiResults.sortedByDescending {
FuzzySearch.ratio(
it.name.lowercase(),
mediaObj.nameRomaji.lowercase()
)
}
} else {
emptyList()
}
@ -84,8 +103,14 @@ abstract class BaseParser {
logger("No exact match found in results. Using closest match from RomajiResults.")
closestRomaji
} else {
val romajiRatio = FuzzySearch.ratio(closestRomaji?.name?.lowercase() ?: "", mediaObj.nameRomaji.lowercase())
val mainNameRatio = FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase())
val romajiRatio = FuzzySearch.ratio(
closestRomaji?.name?.lowercase() ?: "",
mediaObj.nameRomaji.lowercase()
)
val mainNameRatio = FuzzySearch.ratio(
response.name.lowercase(),
mediaObj.mainName().lowercase()
)
logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name.lowercase()}")
logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name?.lowercase() ?: "None"}")
@ -119,7 +144,13 @@ abstract class BaseParser {
open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) {
if (response != null) {
checkIfVariablesAreEmpty()
setUserText("${if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(R.string.found)} : ${response.name}")
setUserText(
"${
if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(
R.string.found
)
} : ${response.name}"
)
saveData("${saveName}_$mediaId", response)
}
}
@ -167,7 +198,7 @@ data class ShowResponse(
val total: Int? = null,
//In case you want to sent some extra data
val extra : MutableMap<String,String>?=null,
val extra: MutableMap<String, String>? = null,
//SAnime object from Aniyomi
val sAnime: SAnime? = null,
@ -175,10 +206,23 @@ data class ShowResponse(
//SManga object from Aniyomi
val sManga: SManga? = null
) : Serializable {
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf(), total: Int? = null, extra: MutableMap<String, String>?=null)
constructor(
name: String,
link: String,
coverUrl: String,
otherNames: List<String> = listOf(),
total: Int? = null,
extra: MutableMap<String, String>? = null
)
: this(name, link, FileUrl(coverUrl), otherNames, total, extra)
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf(), total: Int? = null)
constructor(
name: String,
link: String,
coverUrl: String,
otherNames: List<String> = listOf(),
total: Int? = null
)
: this(name, link, FileUrl(coverUrl), otherNames, total)
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf())

View file

@ -2,9 +2,9 @@ package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga

View file

@ -1,8 +1,6 @@
package ani.dantotsu.parsers
import android.graphics.Bitmap
import ani.dantotsu.FileUrl
import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.MangaNameAdapter
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page
@ -15,7 +13,11 @@ abstract class MangaParser : BaseParser() {
/**
* 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>?, sManga: SManga): 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
@ -23,9 +25,14 @@ abstract class MangaParser : BaseParser() {
* Returns the latest chapter (If overriding, Make sure the chapter is actually the latest chapter)
* Returns null, if no latest chapter is found.
* **/
open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, sManga: SManga, latest: Float): MangaChapter? {
val chapter = loadChapters(mangaLink, extra, sManga)
val max = chapter
open suspend fun getLatestChapter(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga,
latest: Float
): MangaChapter? {
val chapter = loadChapters(mangaLink, extra, sManga)
val max = chapter
.maxByOrNull { MangaNameAdapter.findChapterNumber(it.number) ?: 0f }
return max
?.takeIf { latest < (MangaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
@ -40,13 +47,18 @@ abstract class MangaParser : BaseParser() {
open fun getTransformation(): BitmapTransformation? = null
}
class EmptyMangaParser: MangaParser() {
class EmptyMangaParser : MangaParser() {
override val name: String = "None"
override val saveName: String = "None"
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> = emptyList()
override suspend fun loadChapters(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> = emptyList()
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> = emptyList()
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> =
emptyList()
override suspend fun search(query: String): List<ShowResponse> = emptyList()
}
@ -82,7 +94,7 @@ data class MangaImage(
val useTransformation: Boolean = false,
val page: Page? = null,
) : Serializable{
constructor(url: String,useTransformation: Boolean=false, page: Page? = null)
: this(FileUrl(url),useTransformation, page)
) : Serializable {
constructor(url: String, useTransformation: Boolean = false, page: Page? = null)
: this(FileUrl(url), useTransformation, page)
}

View file

@ -15,11 +15,17 @@ object MangaSources : MangaReadSources() {
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
list = createParsersFromExtensions(initialExtensions) + Lazier(
{ OfflineMangaParser() },
"Downloaded"
)
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
list = createParsersFromExtensions(extensions) + Lazier(
{ OfflineMangaParser() },
"Downloaded"
)
}
}
@ -34,7 +40,8 @@ object MangaSources : MangaReadSources() {
object HMangaSources : MangaReadSources() {
val aList: List<Lazier<BaseParser>> = lazyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
//todo
//todo
}
override val list = listOf(aList,MangaSources.list).flatten()
override val list = listOf(aList, MangaSources.list).flatten()
}

View file

@ -1,4 +1,5 @@
package ani.dantotsu.parsers
import com.lagradost.nicehttp.Requests

View file

@ -9,7 +9,7 @@ abstract class NovelParser : BaseParser() {
abstract suspend fun loadBook(link: String, extra: Map<String, String>?): Book
fun List<ShowResponse>.sortByVolume(query:String) : List<ShowResponse> {
fun List<ShowResponse>.sortByVolume(query: String): List<ShowResponse> {
val sorted = groupBy { res ->
val match = volumeRegex.find(res.name)?.groupValues
?.firstOrNull { it.isNotEmpty() }
@ -42,7 +42,12 @@ data class Book(
val description: String? = null,
val links: List<FileUrl>
) {
constructor (name: String, img: String, description: String? = null, links: List<String>) : this(
constructor (
name: String,
img: String,
description: String? = null,
links: List<String>
) : this(
name,
FileUrl(img),
description,

View file

@ -2,10 +2,10 @@ package ani.dantotsu.parsers
import android.util.Log
import ani.dantotsu.Lazier
import ani.dantotsu.parsers.novel.DynamicNovelParser
import ani.dantotsu.parsers.novel.NovelExtension
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import ani.dantotsu.parsers.novel.DynamicNovelParser
object NovelSources : NovelReadSources() {
override var list: List<Lazier<BaseParser>> = emptyList()

View file

@ -12,7 +12,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineMangaParser: MangaParser() {
class OfflineMangaParser : MangaParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val hostUrl: String = "Offline"
@ -32,7 +32,14 @@ class OfflineMangaParser: MangaParser() {
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(it.name, "$mangaLink/${it.name}", it.name, null, null, SChapter.create())
val chapter = MangaChapter(
it.name,
"$mangaLink/${it.name}",
it.name,
null,
null,
SChapter.create()
)
chapters.add(chapter)
}
}

View file

@ -58,10 +58,16 @@ class StringMatcher {
return shows // Return original list if no closest show found
}
logger("Closest show found for $target is ${closestShowAndIndex.first.name}")
return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList(closestIndex + 1, shows.size)
return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList(
closestIndex + 1,
shows.size
)
}
private fun closestShow(target: String, shows: List<ShowResponse>): Pair<ShowResponse, Int> {
private fun closestShow(
target: String,
shows: List<ShowResponse>
): Pair<ShowResponse, Int> {
var minDistance = Int.MAX_VALUE
var closestShow = ShowResponse("", "", "")
var closestIndex = -1

View file

@ -56,14 +56,19 @@ abstract class VideoExtractor : Serializable {
data class VideoServer(
val name: String,
val embed: FileUrl,
val extraData : Map<String,String>?=null,
val extraData: Map<String, String>? = null,
val video: eu.kanade.tachiyomi.animesource.model.Video? = null
) : Serializable {
constructor(name: String, embedUrl: String,extraData: Map<String,String>?=null)
: this(name, FileUrl(embedUrl),extraData)
constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null)
: this(name, FileUrl(embedUrl), extraData)
constructor(name: String, embedUrl: String,extraData: Map<String,String>?=null, video: eu.kanade.tachiyomi.animesource.model.Video?)
: this(name, FileUrl(embedUrl),extraData, video)
constructor(
name: String,
embedUrl: String,
extraData: Map<String, String>? = null,
video: eu.kanade.tachiyomi.animesource.model.Video?
)
: this(name, FileUrl(embedUrl), extraData, video)
}
/**
@ -117,7 +122,13 @@ data class Video(
val extraNote: String? = null,
) : Serializable {
constructor(quality: Int? = null, videoType: VideoType, url: String, size: Double?, extraNote: String? = null)
constructor(
quality: Int? = null,
videoType: VideoType,
url: String,
size: Double?,
extraNote: String? = null
)
: this(quality, videoType, FileUrl(url), size, extraNote)
constructor(quality: Int? = null, videoType: VideoType, url: String, size: Double?)
@ -149,15 +160,19 @@ data class Subtitle(
*
* Supports VTT, SRT & ASS
* **/
val type:SubtitleType = SubtitleType.VTT,
val type: SubtitleType = SubtitleType.VTT,
) : Serializable {
constructor(language: String, url: String, type: SubtitleType = SubtitleType.VTT) : this(language, FileUrl(url), type)
constructor(language: String, url: String, type: SubtitleType = SubtitleType.VTT) : this(
language,
FileUrl(url),
type
)
}
enum class VideoType{
enum class VideoType {
CONTAINER, M3U8, DASH
}
enum class SubtitleType{
enum class SubtitleType {
VTT, ASS, SRT, UNKNOWN
}

View file

@ -1,6 +1,5 @@
package ani.dantotsu.parsers.novel
import ani.dantotsu.FileUrl
import ani.dantotsu.parsers.Book
import ani.dantotsu.parsers.NovelInterface
import ani.dantotsu.parsers.NovelParser
@ -9,13 +8,14 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class NovelAdapter {
}
class NovelAdapter
class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() {
override val volumeRegex = Regex("vol\\.? (\\d+(\\.\\d+)?)|volume (\\d+(\\.\\d+)?)", RegexOption.IGNORE_CASE)
override val volumeRegex =
Regex("vol\\.? (\\d+(\\.\\d+)?)|volume (\\d+(\\.\\d+)?)", RegexOption.IGNORE_CASE)
var extension: NovelExtension.Installed
val client = Injekt.get<NetworkHelper>().requestClient
init {
this.extension = extension
}

View file

@ -7,7 +7,6 @@ import ani.dantotsu.logger
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
@ -27,7 +26,9 @@ class NovelExtensionGithubApi {
private val novelExtensionManager: NovelExtensionManager by injectLazy()
private val json: Json by injectLazy()
private val lastExtCheck: Long = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getLong("last_ext_check", 0)?:0
private val lastExtCheck: Long =
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getLong("last_ext_check", 0) ?: 0
private var requiresFallbackSource = false
@ -72,7 +73,10 @@ class NovelExtensionGithubApi {
}
}
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
if (fromAvailableExtensionList && Date().time < lastExtCheck + 1.days.inWholeMilliseconds) {
return null
@ -81,7 +85,10 @@ class NovelExtensionGithubApi {
val extensions = if (fromAvailableExtensionList) {
novelExtensionManager.availableExtensionsFlow.value
} else {
findExtensions().also { context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()?.putLong("last_ext_check", Date().time)?.apply() }
findExtensions().also {
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putLong("last_ext_check", Date().time)?.apply()
}
}
val installedExtensions = NovelExtensionLoader.loadExtensions(context)
@ -143,6 +150,7 @@ class NovelExtensionGithubApi {
fun getApkUrl(extension: NovelExtension.Available): String {
return "${getUrlPrefix()}apk/${extension.pkgName}.apk"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
@ -152,8 +160,11 @@ class NovelExtensionGithubApi {
}
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/dannovels/novel-extensions/main/"
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/dannovels/novel-extensions@latest/"
private const val REPO_URL_PREFIX =
"https://raw.githubusercontent.com/dannovels/novel-extensions/main/"
private const val FALLBACK_REPO_URL_PREFIX =
"https://gcore.jsdelivr.net/gh/dannovels/novel-extensions@latest/"
@Serializable
private data class NovelExtensionJsonObject(
val name: String,

View file

@ -1,34 +1,18 @@
package ani.dantotsu.parsers.novel
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.FileObserver
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import ani.dantotsu.parsers.novel.FileObserver.fileObserver
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
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
import java.io.File
import java.lang.Exception
class NovelExtensionFileObserver(private val listener: Listener, private val path: String) : FileObserver(path, CREATE or DELETE or MOVED_FROM or MOVED_TO or MODIFY) {
class NovelExtensionFileObserver(private val listener: Listener, private val path: String) :
FileObserver(path, CREATE or DELETE or MOVED_FROM or MOVED_TO or MODIFY) {
init {
fileObserver = this
}
/**
* Starts observing the file changes in the directory.
*/
@ -48,10 +32,12 @@ class NovelExtensionFileObserver(private val listener: Listener, private val pat
Log.e("NovelExtensionFileObserver", "File created: $fullPath")
listener.onExtensionFileCreated(fullPath)
}
DELETE -> {
Log.e("NovelExtensionFileObserver", "File deleted: $fullPath")
listener.onExtensionFileDeleted(fullPath)
}
MODIFY -> {
Log.e("NovelExtensionFileObserver", "File modified: $fullPath")
listener.onExtensionFileModified(fullPath)

View file

@ -91,8 +91,12 @@ internal class NovelExtensionInstaller(private val context: Context) {
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(NovelExtensionInstaller.APK_MIME)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
.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
@ -144,11 +148,14 @@ internal class NovelExtensionInstaller(private val context: Context) {
}
}
fun installApk(downloadId: Long, uri: Uri, context: Context, pkgName: String) : InstallStep {
val sourcePath = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/" + uri.lastPathSegment
val destinationPath = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
fun installApk(downloadId: Long, uri: Uri, context: Context, pkgName: String): InstallStep {
val sourcePath =
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/" + uri.lastPathSegment
val destinationPath =
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
val destinationPathDirectory = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/"
val destinationPathDirectory =
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/"
val destinationPathDirectoryFile = File(destinationPathDirectory)
@ -164,7 +171,7 @@ internal class NovelExtensionInstaller(private val context: Context) {
if (destinationDir?.exists() == false) {
destinationDir.mkdirs()
}
if(destinationDir?.setWritable(true) == false) {
if (destinationDir?.setWritable(true) == false) {
Log.e("Install APK", "Failed to set destinationDir to writable.")
downloadsRelay.call(downloadId to InstallStep.Error)
return InstallStep.Error
@ -186,7 +193,8 @@ internal class NovelExtensionInstaller(private val context: Context) {
}
fun uninstallApk(pkgName: String, context: Context) {
val apkPath = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
val apkPath =
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
val fileToDelete = File(apkPath)
//give write permission to the file
if (fileToDelete.exists() && !fileToDelete.canWrite()) {

View file

@ -11,9 +11,7 @@ import ani.dantotsu.parsers.NovelInterface
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.util.lang.Hash
import tachiyomi.core.util.system.logcat
import java.io.File
import java.util.Locale
@ -27,7 +25,10 @@ internal object NovelExtensionLoader {
val results = mutableListOf<NovelLoadResult>()
//the number of files
Log.e("NovelExtensionLoader", "Loading extensions from $installDir")
Log.e("NovelExtensionLoader", "Loading extensions from ${File(installDir).listFiles()?.size}")
Log.e(
"NovelExtensionLoader",
"Loading extensions from ${File(installDir).listFiles()?.size}"
)
File(installDir).setWritable(false)
File(installDir).listFiles()?.forEach {
//set the file to read only
@ -48,7 +49,8 @@ internal object NovelExtensionLoader {
* contains the required feature flag before trying to load it.
*/
fun loadExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult {
val path = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
val path =
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk"
//make /extensions/novel read only
context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/".let {
File(it).setWritable(false)
@ -67,7 +69,10 @@ internal object NovelExtensionLoader {
@Suppress("DEPRECATION")
fun loadExtension(context: Context, file: File): NovelLoadResult {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
context.packageManager.getPackageArchiveInfo(file.absolutePath, GET_SIGNING_CERTIFICATES)
context.packageManager.getPackageArchiveInfo(
file.absolutePath,
GET_SIGNING_CERTIFICATES
)
?: return NovelLoadResult.Error(Exception("Failed to load extension"))
} else {
context.packageManager.getPackageArchiveInfo(file.absolutePath, GET_SIGNATURES)
@ -75,8 +80,8 @@ internal object NovelExtensionLoader {
}
val appInfo = packageInfo.applicationInfo
?: return NovelLoadResult.Error(Exception("Failed to load Extension Info"))
appInfo.sourceDir = file.absolutePath;
appInfo.publicSourceDir = file.absolutePath;
appInfo.sourceDir = file.absolutePath
appInfo.publicSourceDir = file.absolutePath
val signatureHash = getSignatureHash(packageInfo)
@ -88,13 +93,14 @@ internal object NovelExtensionLoader {
}
val extension = NovelExtension.Installed(
packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString() ?:
return NovelLoadResult.Error(Exception("Failed to load Extension Info")),
packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString()
?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")),
packageInfo.packageName
?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")),
packageInfo.versionName ?: "",
packageInfo.versionCode.toLong() ?: 0,
loadSources(context, file,
packageInfo.versionCode.toLong(),
loadSources(
context, file,
packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString()!!
),
packageInfo.applicationInfo?.loadIcon(context.packageManager)
@ -126,7 +132,8 @@ internal object NovelExtensionLoader {
}
Log.e("NovelExtensionLoader", "isFileWritable: ${file.canWrite()}")
val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader)
val className = "some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
val className =
"some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
val loadedClass = classLoader.loadClass(className)
val instance = loadedClass.newInstance()
val novelInterfaceInstance = instance as? NovelInterface

View file

@ -2,7 +2,6 @@ package ani.dantotsu.parsers.novel
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import ani.dantotsu.logger
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.extension.InstallStep
@ -51,7 +50,7 @@ class NovelExtensionManager(private val context: Context) {
init {
initNovelExtensions()
val path = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/"
NovelExtensionFileObserver(NovelInstallationListener(),path).register()
NovelExtensionFileObserver(NovelInstallationListener(), path).register()
}
private fun initNovelExtensions() {
@ -129,8 +128,9 @@ class NovelExtensionManager(private val context: Context) {
* @param extension The anime extension to be updated.
*/
fun updateExtension(extension: NovelExtension.Installed): Observable<InstallStep> {
val availableExt = _availableNovelExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
val availableExt =
_availableNovelExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return Observable.empty()
return installExtension(availableExt)
}
@ -192,7 +192,8 @@ class NovelExtensionManager(private val context: Context) {
* @param pkgName The package name of the uninstalled application.
*/
private fun unregisterNovelExtension(pkgName: String) {
val installedNovelExtension = _installedNovelExtensionsFlow.value.find { it.pkgName == pkgName }
val installedNovelExtension =
_installedNovelExtensionsFlow.value.find { it.pkgName == pkgName }
if (installedNovelExtension != null) {
_installedNovelExtensionsFlow.value -= installedNovelExtension
}
@ -210,10 +211,12 @@ class NovelExtensionManager(private val context: Context) {
}
}
}
override fun onExtensionFileDeleted(file: File) {
val pkgName = file.nameWithoutExtension
unregisterNovelExtension(pkgName)
}
override fun onExtensionFileModified(file: File) {
NovelExtensionLoader.loadExtension(context, file).let {
if (it is NovelLoadResult.Success) {
@ -235,7 +238,8 @@ class NovelExtensionManager(private val context: Context) {
}
private fun NovelExtension.Installed.updateExists(availableNovelExtension: NovelExtension.Available? = null): Boolean {
val availableExt = availableNovelExtension ?: _availableNovelExtensionsFlow.value.find { it.pkgName == pkgName }
val availableExt = availableNovelExtension
?: _availableNovelExtensionsFlow.value.find { it.pkgName == pkgName }
if (isUnofficial || availableExt == null) return false
return (availableExt.versionCode > versionCode)