manga "working" :D

This commit is contained in:
Finnley Somdahl 2023-10-20 01:44:36 -05:00
parent 57a584a820
commit 41b90e3a39
32 changed files with 1179 additions and 409 deletions

View file

@ -15,6 +15,8 @@ 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
@ -29,9 +31,11 @@ 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.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@ -39,6 +43,10 @@ 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 {
@ -60,61 +68,59 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
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) {
// 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()
if (source is AnimeCatalogueSource) {
val video = source.getVideoList(sEpisode)
var VideoList: List<VideoServer> = emptyList()
for (videoServer in video) {
VideoList += VideoToVideoServer(videoServer)
}
return VideoList
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()
}
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) {
val source = extension.sources.first() as? AnimeCatalogueSource ?: return emptyList()
var res: AnimesPage? = null
try {
res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first()
logger("res observable: $res")
}
catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
//toast
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()
}
val conv = convertAnimesPageToShowResponse(res!!)
return conv
emptyList()
} catch (e: Exception) {
logger("General exception in search: $e")
emptyList()
}
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
}
private fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> {
return animesPage.animes.map { sAnime ->
// Extract required fields from sAnime
@ -161,6 +167,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed
init {
this.extension = extension
@ -170,68 +177,128 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
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()
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()
}
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 source = extension.sources.first() as? HttpSource ?: return emptyList()
return coroutineScope {
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)
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))
pageToMangaImage(page)
}
}
logger("image url: chapterList size: ${chapterList.size}")
return chapterList
//}
//catch (e: Exception) {
// logger("loadImages Exception: $e")
//}
return emptyList()
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
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
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch {
try {
res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first()
logger("res observable: $res")
// 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}")
}
catch (e: CloudflareBypassException) {
logger("Exception in search: $e")
}
}
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()
}
val conv = convertMangasPageToShowResponse(res!!)
return conv
emptyList()
} catch (e: Exception) {
logger("General exception in search: $e")
emptyList()
}
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
@ -239,7 +306,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val link = sManga.url
val coverUrl = sManga.thumbnail_url ?: ""
val otherNames = emptyList<String>() // Populate as needed
val total = 20
val total = 1
val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse
@ -247,23 +314,32 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
}
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}")
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,
@ -271,55 +347,81 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
)
}
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,
number,
sChapter.url,
sChapter.name,
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() {
return videoServer
}
get() = 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()) {
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
return if (vidList.isNotEmpty()) {
VideoContainer(vidList, subList)
} else {
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
// 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)
var format = when {
path.endsWith(".mp4", ignoreCase = true) || videoUrl.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
path.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
path.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> null
}
if (format == null) {
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")
@ -330,13 +432,9 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
// Assume the file is named under the "file" query parameter
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = 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
}
format = getVideoType(fileName)
}
// If the format is still undetermined, log an error or handle it appropriately
if (format == null) {
logger("Unknown video format: $videoUrl")
@ -353,6 +451,15 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
)
}
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)
}