Files
Dantotsu/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
Finnley Somdahl dc959796e6 various bugfixes
2023-10-22 02:28:39 -05:00

504 lines
18 KiB
Kotlin

package ani.dantotsu.parsers
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
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 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.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
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
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
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.URLDecoder
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.*
import java.io.UnsupportedEncodingException
import java.util.regex.Pattern
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 val isNSFW = extension.isNsfw
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
val source = extension.sources.first()
if (source is AnimeCatalogueSource) {
try {
val res = source.getEpisodeList(sAnime)
// Sort episodes by episode_number
val sortedEpisodes = res.sortedBy { it.episode_number }
// Transform SEpisode objects to Episode objects
return sortedEpisodes.map { SEpisodeToEpisode(it) }
} 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() as? AnimeCatalogueSource ?: return emptyList()
return try {
val videos = source.getVideoList(sEpisode)
videos.map { VideoToVideoServer(it) }
} catch (e: Exception) {
logger("Exception occurred: ${e.message}")
emptyList()
}
}
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
return VideoServerPassthrough(server)
}
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
return try {
val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
}
emptyList()
} catch (e: Exception) {
logger("General exception in search: $e")
emptyList()
}
}
private 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)
}
}
private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
//if the float episode number is a whole number, convert it to an int
val episodeNumberInt =
if (sEpisode.episode_number % 1 == 0f) {
sEpisode.episode_number.toInt()
} else {
sEpisode.episode_number
}
return Episode(
episodeNumberInt.toString(),
sEpisode.url,
sEpisode.name,
null,
null,
false,
null,
sEpisode
)
}
private fun VideoToVideoServer(video: Video): VideoServer {
return VideoServer(
video.quality,
video.url,
null,
video
)
}
}
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed
init {
this.extension = extension
}
override val name = extension.name
override val saveName = extension.name
override val hostUrl = extension.sources.first().name
override val isNSFW = extension.isNsfw
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> {
val source = extension.sources.first() as? CatalogueSource ?: return emptyList()
return try {
val res = source.getChapterList(sManga)
val reversedRes = res.reversed()
val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
logger("chapterList size: ${chapterList.size}")
logger("chapterList: ${chapterList[1].title}")
logger("chapterList: ${chapterList[1].description}")
chapterList
} catch (e: Exception) {
logger("loadChapters Exception: $e")
emptyList()
}
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val source = extension.sources.first() as? HttpSource ?: return emptyList()
return coroutineScope {
try {
println("source.name " + source.name)
val res = source.getPageList(sChapter)
val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) {
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
logger("put page: ${page.imageUrl}")
pageToMangaImage(page)
}
}
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT).show()
emptyList()
}
}
}
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? {
return withContext(Dispatchers.IO) {
try {
// Fetch the image
val response = httpSource.getImage(page)
println("Response: ${response.code}")
println("Response: ${response.message}")
// Convert the Response to an InputStream
val inputStream = response.body?.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
ani.dantotsu.media.manga.saveImage(
bitmap,
context.contentResolver,
page.imageUrl!!,
Bitmap.CompressFormat.JPEG,
100
)
return@withContext bitmap
} catch (e: Exception) {
// Handle any exceptions
println("An error occurred: ${e.message}")
return@withContext null
}
}
}
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch {
try {
// Fetch the image
val response = httpSource.getImage(page)
// Convert the Response to an InputStream
val inputStream = response.body?.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
withContext(Dispatchers.IO) {
// Save the Bitmap using MediaStore API
saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100)
}
inputStream?.close()
} catch (e: Exception) {
// Handle any exceptions
println("An error occurred: ${e.message}")
}
}
}
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime")
}
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
uri?.let {
contentResolver.openOutputStream(it)?.use { os ->
bitmap.compress(format, quality, os)
}
}
} else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
if (!directory.exists()) {
directory.mkdirs()
}
val file = File(directory, filename)
FileOutputStream(file).use { outputStream ->
bitmap.compress(format, quality, outputStream)
}
}
} catch (e: Exception) {
// Handle exception here
println("Exception while saving image: ${e.message}")
}
}
override suspend fun search(query: String): List<ShowResponse> {
val source = extension.sources.first() as? HttpSource ?: return emptyList()
return try {
val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
logger("res observable: $res")
convertMangasPageToShowResponse(res)
} catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show()
}
emptyList()
} catch (e: Exception) {
logger("General exception in search: $e")
emptyList()
}
}
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 = 1
val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse
ShowResponse(name, link, coverUrl, sManga)
}
}
private fun pageToMangaImage(page: Page): MangaImage {
var headersMap = mapOf<String, String>()
var urlWithoutHeaders = ""
var url = ""
page.imageUrl?.let {
val splitUrl = it.split("&")
urlWithoutHeaders = splitUrl.getOrNull(0) ?: ""
url = it
headersMap = splitUrl.mapNotNull { part ->
val idx = part.indexOf("=")
if (idx != -1) {
try {
val key = URLDecoder.decode(part.substring(0, idx), "UTF-8")
val value = URLDecoder.decode(part.substring(idx + 1), "UTF-8")
Pair(key, value)
} catch (e: UnsupportedEncodingException) {
null
}
} else {
null
}
}.toMap()
}
return MangaImage(
FileUrl(url, headersMap),
false,
page
)
}
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
/*val parsedChapterTitle = parseChapterTitle(sChapter.name)
val number = if (sChapter.chapter_number.toInt() != -1){
sChapter.chapter_number.toString()
} else if(parsedChapterTitle.first != null || parsedChapterTitle.second != null){
(parsedChapterTitle.first ?: "") + "." + (parsedChapterTitle.second ?: "")
}else{
sChapter.name
}*/
return MangaChapter(
sChapter.name,
sChapter.url,
//if (parsedChapterTitle.first != null || parsedChapterTitle.second != null) {
// parsedChapterTitle.third
//} else {
sChapter.name,
//},
null,
sChapter
)
}
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val volumeMatcher = volumePattern.matcher(title)
val chapterMatcher = chapterPattern.matcher(title)
val volumeNumber = if (volumeMatcher.find()) volumeMatcher.group(1) else null
val chapterNumber = if (chapterMatcher.find()) chapterMatcher.group(1) else null
var remainingTitle = title
if (volumeNumber != null) {
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
}
if (chapterNumber != null) {
remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
}
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
}
}
class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer
get() = videoServer
override suspend fun extract(): VideoContainer {
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
return if (vidList.isNotEmpty()) {
VideoContainer(vidList, subList)
} else {
throw Exception("No videos found")
}
}
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
// Find the number value from the .quality string
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
// Check for null video URL
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
val urlObj = URL(videoUrl)
val path = urlObj.path
val query = urlObj.query
var format = getVideoType(path)
if (format == null && query != null) {
val queryPairs: List<Pair<String, String>> = query.split("&").map {
val idx = it.indexOf("=")
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8")
Pair(key, value)
}
// Assume the file is named under the "file" query parameter
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName)
}
// If the format is still undetermined, log an error or handle it appropriately
if (format == null) {
logger("Unknown video format: $videoUrl")
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 getVideoType(fileName: String): VideoType? {
return when {
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null
}
}
private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle {
return Subtitle(track.lang, track.url, type)
}
}