Initial commit

This commit is contained in:
Finnley Somdahl 2023-10-17 18:42:43 -05:00
commit 21bfbfb139
520 changed files with 47819 additions and 0 deletions

View file

@ -0,0 +1,224 @@
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.asyncMap
import ani.dantotsu.currContext
import ani.dantotsu.loadData
import ani.dantotsu.others.MalSyncBackup
import ani.dantotsu.saveData
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlin.properties.Delegates
/**
* An abstract class for creating a new Source
*
* Most of the functions & variables that need to be overridden are abstract
* **/
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>
/**
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Episode Number known by app as arguments
*
* 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?{
return loadEpisodes(animeLink, extra, sAnime)
.maxByOrNull { it.number.toFloatOrNull()?:0f }
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
}
/**
* Takes Episode.link as a parameter
*
* 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>
/**
* This function will receive **url of the embed** & **name** of a Video Server present on the site to host the episode.
*
*
* Create a new VideoExtractor for the video server you are trying to scrape, if there's not one already.
*
*
* (Some sites might not have separate video hosts. In that case, just create a new VideoExtractor for that particular site)
*
*
* returns a **VideoExtractor** containing **`server`**, the app will further load the videos using `extract()` function inside it
*
* **Example for Site with multiple Video Servers**
* ```
val domain = Uri.parse(server.embed.url).host ?: ""
val extractor: VideoExtractor? = when {
"fembed" in domain -> FPlayer(server)
"sb" in domain -> StreamSB(server)
"streamta" in domain -> StreamTape(server)
else -> null
}
return extractor
```
* You can use your own way to get the Extractor for reliability.
* if there's only extractor, you can directly return it.
* **/
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)}
val extractor: VideoExtractor? = when (domain) {
else -> {
println("$name : No extractor found for: $domain | ${server.embed.url}")
null
}
}
return extractor
}
/**
* If the Video Servers support preloading links for the videos
* typically depends on what Video Extractor is being used
* **/
open val allowsPreloading = true
/**
* This Function used when there "isn't" a default Server set by the user, or when user wants to switch the Server
*
* 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) {
tryWithSuspend(true) {
loadVideoServers(episodeUrl, extra, sEpisode).asyncMap {
getVideoExtractor(it)?.apply {
tryWithSuspend(true) {
load()
}
callback.invoke(this)
}
}
}
}
/**
* This Function used when there "is" a default Server set by the user, only loads a Single Server for faster response.
*
* 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? {
return tryWithSuspend(post) {
loadVideoServers(episodeUrl, extra, sEpisode).apply {
find { it.name == serverName }?.also {
return@tryWithSuspend getVideoExtractor(it)?.apply {
load()
}
}
}
null
}
}
/**
* Many sites have Dub & Sub anime as separate Shows
*
* make this `true`, if they are separated else `false`
*
* **NOTE : do not forget to override `search` if the site does not support only dub search**
* **/
open val isDubAvailableSeparately by Delegates.notNull<Boolean>()
/**
* The app changes this, depending on user's choice.
* **/
open var selectDub = false
/**
* Name used to get Shows Directly from MALSyncBackup's github dump
*
* Do not override if the site is not present on it.
* **/
open val malSyncBackupName = ""
/**
* Overridden to add MalSyncBackup support for Anime Sites
* **/
override suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
checkIfVariablesAreEmpty()
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) }
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}")
val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else ""
saveData("${saveName}${dub}_$mediaId", response)
}
}
}
/**
* A class for containing Episode data of a particular parser
* **/
data class Episode(
/**
* Number of the Episode in "String",
*
* useful in cases where episode is not a number
* **/
val number: String,
/**
* Link that links to the episode page containing videos
* **/
val link: String,
//Self-Descriptive
val title: String? = null,
val thumbnail: FileUrl? = null,
val description: String? = null,
val isFiller: Boolean = false,
/**
* In case, you want to pass extra data
* **/
val extra: Map<String,String>? = null,
//SEpisode from Aniyomi
val sEpisode: SEpisode? = null
) {
constructor(
number: String,
link: String,
title: String? = null,
thumbnail: String,
description: String? = null,
isFiller: Boolean = false,
extra: Map<String,String>? = null
) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra)
constructor(
number: String,
link: String,
title: String? = null,
thumbnail: String,
description: String? = null,
isFiller: Boolean = false,
extra: Map<String,String>? = null,
sEpisode: SEpisode? = null
) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra, sEpisode)
}

View file

@ -0,0 +1,65 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
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.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() {
override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.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<AnimeExtension.Installed>): List<Lazier<BaseParser>> {
return extensions.map { extension ->
val name = extension.name
Lazier({ DynamicAnimeParser(extension) }, name)
}
}
}
object HAnimeSources : WatchSources() {
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()
}

View file

@ -0,0 +1,175 @@
package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
import ani.dantotsu.logger
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
class AniyomiAdapter {
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
return DynamicAnimeParser(extension)
}
}
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val extension: AnimeExtension.Installed
init {
this.extension = extension
}
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override val isDubAvailableSeparately = false
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
val source = extension.sources.first()
if (source is AnimeCatalogueSource) {
var res: SEpisode? = null
try {
val res = source.getEpisodeList(sAnime)
var EpisodeList: List<Episode> = emptyList()
for (episode in res) {
println("episode: $episode")
EpisodeList += SEpisodeToEpisode(episode)
}
return EpisodeList
}
catch (e: Exception) {
println("Exception: $e")
}
return emptyList()
}
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
val source = extension.sources.first()
if (source is AnimeCatalogueSource) {
val video = source.getVideoList(sEpisode)
var VideoList: List<VideoServer> = emptyList()
for (videoServer in video) {
VideoList += VideoToVideoServer(videoServer)
}
return VideoList
}
return emptyList()
}
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
return VideoServerPassthrough(server)
}
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first()
if (source is AnimeCatalogueSource) {
var res: AnimesPage? = null
try {
res = source.fetchSearchAnime(0, query, AnimeFilterList()).toBlocking().first()
println("res: $res")
}
catch (e: Exception) {
logger("Exception: $e")
}
val conv = convertAnimesPageToShowResponse(res!!)
return conv
}
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> {
return animesPage.animes.map { sAnime ->
// Extract required fields from sAnime
val name = sAnime.title
val link = sAnime.url
val coverUrl = sAnime.thumbnail_url ?: ""
val otherNames = emptyList<String>() // Populate as needed
val total = 1
val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse
ShowResponse(name, link, coverUrl, sAnime)
}
}
fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
val episode = Episode(
sEpisode.episode_number.toString(),
sEpisode.url,
sEpisode.name,
null,
null,
false,
null,
sEpisode)
return episode
}
fun VideoToVideoServer(video: Video): VideoServer {
val videoServer = VideoServer(
video.quality,
video.url,
null,
video)
return videoServer
}
}
class VideoServerPassthrough : VideoExtractor{
val videoServer: VideoServer
constructor(videoServer: VideoServer) {
this.videoServer = videoServer
}
override val server: VideoServer
get() {
return videoServer
}
override suspend fun extract(): VideoContainer {
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
var subList: List<Subtitle> = emptyList()
for(sub in videoServer.video?.subtitleTracks ?: emptyList()) {
subList += TrackToSubtitle(sub)
}
if(vidList.isEmpty()) {
throw Exception("No videos found")
}else{
return VideoContainer(vidList, subList)
}
}
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
//try to find the number value from the .quality string
val regex = Regex("""\d+""")
val result = regex.find(aniVideo.quality)
val number = result?.value?.toInt() ?: 0
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
val format = when {
videoUrl.endsWith(".mp4", ignoreCase = true) || videoUrl.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
videoUrl.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
videoUrl.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> throw Exception("Unknown video format")
}
val headersMap: Map<String, String> = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
return ani.dantotsu.parsers.Video(
number,
format,
FileUrl(videoUrl, headersMap),
aniVideo.totalContentLength.toDouble()
)
}
private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle {
return Subtitle(track.lang, track.url, type)
}
}

View file

@ -0,0 +1,162 @@
package ani.dantotsu.parsers
import ani.dantotsu.*
import ani.dantotsu.media.Media
import eu.kanade.tachiyomi.animesource.model.SAnime
import java.io.Serializable
import java.net.URLDecoder
import java.net.URLEncoder
abstract class BaseParser {
/**
* Name that will be shown in Source Selection
* **/
open val name: String = ""
/**
* Name used to save the ShowResponse selected by user or by autoSearch
* **/
open val saveName: String = ""
/**
* The main URL of the Site
* **/
open val hostUrl: String = ""
/**
* override as `true` if the site **only** has NSFW media
* **/
open val isNSFW = false
/**
* mostly redundant for official app, But override if you want to add different languages
* **/
open val language = "English"
/**
* Search for Anime/Manga/Novel, returns a List of Responses
*
* use `encode(query)` to encode the query for making requests
* **/
abstract suspend fun search(query: String): List<ShowResponse>
/**
* The function app uses to auto find the anime/manga using Media data provided by anilist
*
* Isn't necessary to override, but recommended, if you want to improve auto search results
* **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id)
if (response != null) {
saveShowResponse(mediaObj.id, response, true)
} else {
setUserText("Searching : ${mediaObj.mainName()}")
val results = search(mediaObj.mainName())
val sortedResults = if (results.isNotEmpty()) {
StringMatcher.closestShowMovedToTop(mediaObj.mainName(), results)
} else {
emptyList()
}
response = sortedResults.firstOrNull()
if (response == null) {
setUserText("Searching : ${mediaObj.nameRomaji}")
val romajiResults = search(mediaObj.nameRomaji)
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
StringMatcher.closestShowMovedToTop(mediaObj.nameRomaji, romajiResults)
} else {
emptyList()
}
response = sortedRomajiResults.firstOrNull()
}
saveShowResponse(mediaObj.id, response)
}
return response
}
/**
* Used to get an existing Search Response which was selected by the user.
* **/
open suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
checkIfVariablesAreEmpty()
return loadData("${saveName}_$mediaId")
}
/**
* Used to save Shows Response using `saveName`.
* **/
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}")
saveData("${saveName}_$mediaId", response)
}
}
fun checkIfVariablesAreEmpty() {
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser")
if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser")
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser")
}
open var showUserText = ""
open var showUserTextListener: ((String) -> Unit)? = null
/**
* Used to show messages & errors to the User, a useful way to convey what's currently happening or what was done.
* **/
fun setUserText(string: String) {
showUserText = string
showUserTextListener?.invoke(showUserText)
}
fun encode(input: String): String = URLEncoder.encode(input, "utf-8").replace("+", "%20")
fun decode(input: String): String = URLDecoder.decode(input, "utf-8")
val defaultImage = "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/default.jpg"
}
/**
* A single show which contains some episodes/chapters which is sent by the site using their search function.
*
* You might wanna include `otherNames` & `total` too, to further improve user experience.
*
* You can also store a Map of Strings if you want to save some extra data.
* **/
data class ShowResponse(
val name: String,
val link: String,
val coverUrl: FileUrl,
//would be Useful for custom search, ig
val otherNames: List<String> = listOf(),
//Total number of Episodes/Chapters in the show.
val total: Int? = null,
//In case you want to sent some extra data
val extra : Map<String,String>?=null,
//SAnime object from Aniyomi
val sAnime: SAnime?=null
) : Serializable {
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)
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())
: this(name, link, FileUrl(coverUrl), otherNames)
constructor(name: String, link: String, coverUrl: String)
: this(name, link, FileUrl(coverUrl))
constructor(name: String, link: String, coverUrl: String, sAnime: SAnime)
: this(name, link, FileUrl(coverUrl), sAnime = sAnime)
}

View file

@ -0,0 +1,110 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
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
abstract class WatchSources : BaseSources() {
override operator fun get(i: Int): AnimeParser {
return (list.getOrNull(i)?:list[0]).get.value as AnimeParser
}
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> {
return tryWithSuspend(true) {
val res = get(i).autoSearch(media) ?: return@tryWithSuspend mutableMapOf()
loadEpisodes(i, res.link, res.extra, res.sAnime)
} ?: mutableMapOf()
}
suspend fun loadEpisodes(i: Int, showLink: String, extra: Map<String, String>?, sAnime: SAnime?): MutableMap<String, Episode> {
println("finder333 $showLink")
val map = mutableMapOf<String, Episode>()
val parser = get(i)
tryWithSuspend(true) {
if (sAnime != null) {
parser.loadEpisodes(showLink,extra, sAnime).forEach {
map[it.number] = Episode(it.number, it.link, it.title, it.description, it.thumbnail, it.isFiller, extra = it.extra, sEpisode = it.sEpisode)
}
}
}
return map
}
}
abstract class MangaReadSources : BaseSources() {
override operator fun get(i: Int): MangaParser {
return (list.getOrNull(i)?:list[0]).get.value as MangaParser
}
suspend fun loadChaptersFromMedia(i: Int, media: Media): MutableMap<String, MangaChapter> {
return tryWithSuspend(true) {
val res = get(i).autoSearch(media) ?: return@tryWithSuspend mutableMapOf()
loadChapters(i, res)
} ?: mutableMapOf()
}
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
val map = mutableMapOf<String, MangaChapter>()
val parser = get(i)
tryWithSuspend(true) {
parser.loadChapters(show.link, show.extra).forEach {
map[it.number] = MangaChapter(it)
}
}
return map
}
}
abstract class NovelReadSources : BaseSources(){
override operator fun get(i: Int): NovelParser? {
return if (list.isNotEmpty()) {
(list.getOrNull(i) ?: list[0]).get.value as NovelParser
} else {
return EmptyNovelParser()
}
}
}
class EmptyNovelParser : NovelParser() {
override val volumeRegex: Regex = Regex("")
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
return Book("","", null, emptyList()) // Return an empty Book object or some default value
}
override suspend fun search(query: String): List<ShowResponse> {
return listOf() // Return an empty list or some default value
}
}
abstract class BaseSources {
abstract val list: List<Lazier<BaseParser>>
val names: List<String> get() = list.map { it.name }
fun flushText() {
list.forEach {
if (it.get.isInitialized())
it.get.value?.showUserText = ""
}
}
open operator fun get(i: Int): BaseParser? {
return list[i].get.value
}
fun saveResponse(i: Int, mediaId: Int, response: ShowResponse) {
get(i)?.saveShowResponse(mediaId, response, true)
}
}

View file

@ -0,0 +1,82 @@
package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.media.Media
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.io.Serializable
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>?): List<MangaChapter>
/**
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Chapter Number known by app as arguments
*
* 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>?, latest: Float): MangaChapter? {
return loadChapters(mangaLink, extra)
.maxByOrNull { it.number.toFloatOrNull() ?: 0f }
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
}
/**
* 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>
override suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id)
if (response != null) {
saveShowResponse(mediaObj.id, response, true)
} else {
setUserText("Searching : ${mediaObj.mangaName()}")
response = search(mediaObj.mangaName()).let { if (it.isNotEmpty()) it[0] else null }
if (response == null) {
setUserText("Searching : ${mediaObj.nameRomaji}")
response = search(mediaObj.nameRomaji).let { if (it.isNotEmpty()) it[0] else null }
}
saveShowResponse(mediaObj.id, response)
}
return response
}
open fun getTransformation(): BitmapTransformation? = null
}
data class MangaChapter(
/**
* Number of the Chapter in "String",
*
* useful in cases where chapter is not a number
* **/
val number: String,
/**
* Link that links to the chapter page containing videos
* **/
val link: String,
//Self-Descriptive
val title: String? = null,
val description: String? = null,
)
data class MangaImage(
/**
* The direct url to the Image of a page in a chapter
*
* Supports jpeg,jpg,png & gif(non animated) afaik
* **/
val url: FileUrl,
val useTransformation: Boolean = false
) : Serializable{
constructor(url: String,useTransformation: Boolean=false)
: this(FileUrl(url),useTransformation)
}

View file

@ -0,0 +1,15 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
object MangaSources : MangaReadSources() {
override val list: List<Lazier<BaseParser>> = lazyList(
)
}
object HMangaSources : MangaReadSources() {
val aList: List<Lazier<BaseParser>> = lazyList(
)
override val list = listOf(aList,MangaSources.list).flatten()
}

View file

@ -0,0 +1,51 @@
package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.media.Media
abstract class NovelParser : BaseParser() {
abstract val volumeRegex: Regex
abstract suspend fun loadBook(link: String, extra: Map<String, String>?): Book
fun List<ShowResponse>.sortByVolume(query:String) : List<ShowResponse> {
val sorted = groupBy { res ->
val match = volumeRegex.find(res.name)?.groupValues
?.firstOrNull { it.isNotEmpty() }
?.substringAfter(" ")
?.toDoubleOrNull() ?: Double.MAX_VALUE
match
}.toSortedMap().values
val volumes = sorted.map { showList ->
val nonDefaultCoverShows = showList.filter { it.coverUrl.url != defaultImage }
val bestShow = nonDefaultCoverShows.firstOrNull { it.name.contains(query) }
?: nonDefaultCoverShows.firstOrNull()
?: showList.first()
bestShow
}
val remainingShows = sorted.flatten() - volumes.toSet()
return volumes + remainingShows
}
suspend fun sortedSearch(mediaObj: Media): List<ShowResponse> {
val query = mediaObj.name ?: mediaObj.nameRomaji
return search(query).sortByVolume(query)
}
}
data class Book(
val name: String,
val img: FileUrl,
val description: String? = null,
val links: List<FileUrl>
) {
constructor (name: String, img: String, description: String? = null, links: List<String>) : this(
name,
FileUrl(img),
description,
links.map { FileUrl(it) }
)
}

View file

@ -0,0 +1,9 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import ani.dantotsu.lazyList
object NovelSources : NovelReadSources() {
override val list: List<Lazier<BaseParser>> = lazyList(
)
}

View file

@ -0,0 +1,77 @@
package ani.dantotsu.parsers
class StringMatcher {
companion object {
private fun levenshteinDistance(s1: String, s2: String): Int {
val dp = Array(s1.length + 1) { IntArray(s2.length + 1) }
for (i in 0..s1.length) {
for (j in 0..s2.length) {
when {
i == 0 -> dp[i][j] = j
j == 0 -> dp[i][j] = i
else -> dp[i][j] = minOf(
dp[i - 1][j - 1] + if (s1[i - 1] == s2[j - 1]) 0 else 1,
dp[i - 1][j] + 1,
dp[i][j - 1] + 1
)
}
}
}
return dp[s1.length][s2.length]
}
fun closestString(target: String, list: List<String>): Pair<String, Int> {
var minDistance = Int.MAX_VALUE
var closestString = ""
var closestIndex = -1
for ((index, str) in list.withIndex()) {
val distance = levenshteinDistance(target, str)
if (distance < minDistance) {
minDistance = distance
closestString = str
closestIndex = index
}
}
return Pair(closestString, closestIndex)
}
fun closestStringMovedToTop(target: String, list: List<String>): List<String> {
val (_, closestIndex) = closestString(target, list)
if (closestIndex == -1) {
return list // Return original list if no closest string found
}
return listOf(list[closestIndex]) + list.subList(0, closestIndex) + list.subList(
closestIndex + 1,
list.size
)
}
fun closestShowMovedToTop(target: String, shows: List<ShowResponse>): List<ShowResponse> {
val closestShowAndIndex = closestShow(target, shows)
val closestIndex = closestShowAndIndex.second
if (closestIndex == -1) {
return shows // Return original list if no closest show found
}
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> {
var minDistance = Int.MAX_VALUE
var closestShow = ShowResponse("", "", "")
var closestIndex = -1
for ((index, show) in shows.withIndex()) {
val distance = levenshteinDistance(target, show.name)
if (distance < minDistance) {
minDistance = distance
closestShow = show
closestIndex = index
}
}
return Pair(closestShow, closestIndex)
}
}
}

View file

@ -0,0 +1,163 @@
package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import java.io.Serializable
/**
* Used to extract videos from a specific video host,
*
* A new instance is created for every embeds/iframes of that Episode
* **/
abstract class VideoExtractor : Serializable {
abstract val server: VideoServer
var videos: List<Video> = listOf()
var subtitles: List<Subtitle> = listOf()
/**
* Extracts videos & subtitles from the `embed`
*
* returns a container containing both videos & subtitles (optional)
* **/
abstract suspend fun extract(): VideoContainer
/**
* Loads videos & subtitles from a given Url
*
* & returns itself with the data loaded
* **/
open suspend fun load(): VideoExtractor {
extract().also {
videos = it.videos
subtitles = it.subtitles
return this
}
}
/**
* Gets called when a Video from this extractor starts playing
*
* Useful for Extractor that require Polling
* **/
open suspend fun onVideoPlayed(video: Video?) {}
/**
* Called when a particular video has been stopped playing
**/
open suspend fun onVideoStopped(video: Video?) {}
}
/**
* A simple class containing name, link & extraData(in case you want to give some to it) of the embed which shows the video present on the site
*
* `name` variable is used when checking if there was a Default Server Selected with the same name
*
*
* **/
data class VideoServer(
val name: String,
val embed: FileUrl,
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, video: eu.kanade.tachiyomi.animesource.model.Video?)
: this(name, FileUrl(embedUrl),extraData, video)
}
/**
* A Container for keeping video & subtitles, so you dont need to check backend
* **/
data class VideoContainer(
val videos: List<Video>,
val subtitles: List<Subtitle> = listOf()
) : Serializable
/**
* The Class which contains all the information about a Video
* **/
data class Video(
/**
* Will represent quality to user in form of `"${quality}p"` (1080p)
*
* If quality is null, shows "Unknown Quality"
*
* If isM3U8 is true, shows "Multi Quality"
* **/
val quality: Int?,
/**
* Mime type / Format of the video,
*
* If not a "CONTAINER" format, the app show video as a "Multi Quality" Link
* "CONTAINER" formats are Mp4 & Mkv
* **/
val format: VideoType,
/**
* The direct url to the Video
*
* Supports mp4, mkv, dash & m3u8, afaik
* **/
val file: FileUrl,
/**
* use getSize(url) to get this size,
*
* no need to set it on M3U8 links
* **/
val size: Double? = null,
/**
* In case, you want to show some extra notes to the User
*
* Ex: "Backup" which could be used if the site provides some
* **/
val extraNote: String? = null,
) : Serializable {
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?)
: this(quality, videoType, FileUrl(url), size)
constructor(quality: Int? = null, videoType: VideoType, url: String)
: this(quality, videoType, FileUrl(url))
}
/**
* The Class which contains the link to a subtitle file of a specific language
* **/
data class Subtitle(
/**
* Language of the Subtitle
*
* for now app will directly try to select "English".
* Probably in rework we can add more subtitles support
* **/
val language: String,
/**
* The direct url to the Subtitle
* **/
val file: FileUrl,
/**
* format of the Subtitle
*
* Supports VTT, SRT & ASS
* **/
val type:SubtitleType = SubtitleType.VTT,
) : Serializable {
constructor(language: String, url: String, type: SubtitleType = SubtitleType.VTT) : this(language, FileUrl(url), type)
}
enum class VideoType{
CONTAINER, M3U8, DASH
}
enum class SubtitleType{
VTT, ASS, SRT
}