author Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992132 -0500
committer Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992691 -0500

manga downloading base

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md
This commit is contained in:
Finnley Somdahl 2023-11-03 01:15:32 -05:00
parent acb0225699
commit 20acd71b1a
24 changed files with 763 additions and 64 deletions

View file

@ -15,9 +15,9 @@ Dantotsu is crafted from the ashes of Saikou and is based on simplistic yet stat
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a> <a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION! ### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
> **WARNING** > **WARNING ⚠️**
> >
> Please do not attempt to upload Dantotsu or any of its forks on Playstore or any other Android app stores on the internet. Doing so may infringe their terms and conditions and result in legal action or immediate take-down of the app. > Please do not attempt to upload Dantotsu or any of its forks on Playstore or any other Android app stores on the internet. Doing so may infringe their terms and conditions and result in legal action or immediate take-down of the app.

View file

@ -62,7 +62,7 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:2.8.1" implementation "androidx.work:work-runtime-ktx:2.8.1"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.github.Blatzar:NiceHttp:0.4.3' implementation 'com.github.Blatzar:NiceHttp:0.4.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference:1.2.1'

View file

@ -267,6 +267,8 @@
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService" <service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false" /> android:exported="false" />
<service android:name=".download.manga.MangaDownloaderService" />
</application> </application>
</manifest> </manifest>

View file

@ -23,11 +23,14 @@ import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import ani.dantotsu.download.DownloadsManager
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }

View file

@ -0,0 +1,115 @@
package ani.dantotsu.download
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.widget.Toast
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.File
import java.io.Serializable
class DownloadsManager(private val context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList()
val mangaDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.MANGA }
val animeDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.ANIME }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply()
}
private fun loadDownloads(): List<Download> {
val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) {
val type = object : TypeToken<List<Download>>() {}.type
gson.fromJson(jsonString, type)
} else {
emptyList()
}
}
fun addDownload(download: Download) {
downloadsList.add(download)
saveDownloads()
}
fun removeDownload(download: Download) {
downloadsList.remove(download)
removeDirectory(download)
saveDownloads()
}
private fun removeDirectory(download: Download) {
val directory = if (download.type == Download.Type.MANGA){
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
}
// Check if the directory exists and delete it recursively
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA){
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}")
}
val destination = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${download.title}/${download.chapter}")
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
if (copied) {
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun purgeDownloads(type: Download.Type){
val directory = if (type == Download.Type.MANGA){
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
}
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
downloadsList.removeAll { it.type == type }
saveDownloads()
}
}
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME
}
}

View file

@ -0,0 +1,273 @@
package ani.dantotsu.download.manga
import android.Manifest
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.ImageData
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
import java.net.HttpURLConnection
import java.net.URL
import androidx.core.content.ContextCompat
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
class MangaDownloaderService : Service() {
private var title: String = ""
private var chapter: String = ""
private var retries: Int = 2
private var simultaneousDownloads: Int = 2
private var imageData: List<ImageData> = listOf()
private var sourceMedia: Media? = null
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var builder: NotificationCompat.Builder
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
return null
}
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(this)
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Manga Download Progress")
setContentText("Downloading $title - $chapter")
setSmallIcon(R.drawable.ic_round_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(0, 0, false)
}
startForeground(NOTIFICATION_ID, builder.build())
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
snackString("Download started")
title = intent?.getStringExtra("title") ?: ""
chapter = intent?.getStringExtra("chapter") ?: ""
retries = intent?.getIntExtra("retries", 2) ?: 2
simultaneousDownloads = intent?.getIntExtra("simultaneousDownloads", 2) ?: 2
imageData = ServiceDataSingleton.imageData
sourceMedia = ServiceDataSingleton.sourceMedia
ServiceDataSingleton.imageData = listOf()
ServiceDataSingleton.sourceMedia = null
CoroutineScope(Dispatchers.Default).launch {
download()
}
return START_NOT_STICKY
}
suspend fun download() {
withContext(Dispatchers.Main) {
if (ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
Toast.makeText(
this@MangaDownloaderService,
"Please grant notification permission",
Toast.LENGTH_SHORT
).show()
return@withContext
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
val deferredList = mutableListOf<Deferred<Bitmap?>>()
// Loop through each ImageData object
var farthest = 0
for ((index, image) in imageData.withIndex()) {
// Limit the number of simultaneous downloads
if (deferredList.size >= simultaneousDownloads) {
// Wait for all deferred to complete and clear the list
deferredList.awaitAll()
deferredList.clear()
}
// Download the image and add to deferred list
val deferred = async(Dispatchers.IO) {
var bitmap: Bitmap? = null
var retryCount = 0
while (bitmap == null && retryCount < retries) {
bitmap = imageData[index].fetchAndProcessImage(
imageData[index].page,
imageData[index].source,
this@MangaDownloaderService
)
retryCount++
}
// Cache the image if successful
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap)
}
farthest++
builder.setProgress(imageData.size, farthest + 1, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
bitmap
}
deferredList.add(deferred)
}
// Wait for any remaining deferred to complete
deferredList.awaitAll()
builder.setContentText("Download complete")
.setProgress(0, 0, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
saveMediaInfo()
downloadsManager.addDownload(Download(title, chapter, Download.Type.MANGA))
downloadsManager.exportDownloads(Download(title, chapter, Download.Type.MANGA))
broadcastDownloadFinished(chapter)
snackString("Download finished")
stopSelf()
}
}
fun saveToDisk(fileName: String, bitmap: Bitmap) {
try {
// Define the directory within the private external storage space
val directory = File(
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
if (!directory.exists()) {
directory.mkdirs()
}
// Create a file reference within that directory for your image
val file = File(directory, fileName)
// Use a FileOutputStream to write the bitmap to the file
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
Toast.makeText(this, "Exception while saving image: ${e.message}", Toast.LENGTH_LONG)
.show()
}
}
fun saveMediaInfo() {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val mediaJson = gson.toJson(sourceMedia) //need a deep copy of sourceMedia
val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
}
}
}
}
suspend fun downloadImage(url: String, directory: File, name: String): String? = withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(this@MangaDownloaderService, "Exception while saving ${name}: ${e.message}", Toast.LENGTH_LONG).show()
}
null
} finally {
connection?.disconnect()
}
}
private fun broadcastDownloadStarted(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFinished(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
companion object {
private const val NOTIFICATION_ID = 1103
}
}
object ServiceDataSingleton {
var imageData: List<ImageData> = listOf()
var sourceMedia: Media? = null
}

View file

@ -28,6 +28,9 @@ import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.* import java.util.concurrent.*
@ -118,6 +121,9 @@ object Helper {
val database = StandaloneDatabaseProvider(context) val database = StandaloneDatabaseProvider(context)
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory { val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
val networkHelper = Injekt.get<NetworkHelper>()
val okHttpClient = networkHelper.client
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach { defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value) dataSource.setRequestProperty(it.key, it.value)

View file

@ -24,7 +24,7 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification = override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification( DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this, this,
R.drawable.monochrome, R.drawable.mono,
null, null,
null, null,
downloads, downloads,

View file

@ -22,7 +22,7 @@ data class Media(
val userPreferredName: String, val userPreferredName: String,
var cover: String? = null, var cover: String? = null,
val banner: String? = null, var banner: String? = null,
var relation: String? = null, var relation: String? = null,
var popularity: Int? = null, var popularity: Int? = null,

View file

@ -3,12 +3,14 @@ package ani.dantotsu.media
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.FileUrl
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.anime.SelectorDialogFragment
@ -30,6 +32,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.AniyomiAdapter import ani.dantotsu.parsers.AniyomiAdapter
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
@ -44,6 +48,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
class MediaDetailsViewModel : ViewModel() { class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true) val scrolledToTop = MutableLiveData(true)
@ -258,7 +263,28 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapter = MutableLiveData<MangaChapter?>(null) private val mangaChapter = MutableLiveData<MangaChapter?>(null)
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean { suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean {
//check if the chapter has been downloaded already
val downloadsManager = Injekt.get<DownloadsManager>()
if(downloadsManager.mangaDownloads.contains(Download(series, chapter.title!!, Download.Type.MANGA))) {
val download = downloadsManager.mangaDownloads.find { it.title == series && it.chapter == chapter.title!! } ?: return false
//look in the downloads folder for the chapter and add all the numerically named images to the chapter
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$series/${chapter.title!!}"
)
val images = mutableListOf<MangaImage>()
directory.listFiles()?.forEach {
if (it.nameWithoutExtension.toIntOrNull() != null) {
images.add(MangaImage(FileUrl(it.absolutePath), false))
}
}
//sort the images by name
images.sortBy { it.url.url }
chapter.addImages(images)
if (post) mangaChapter.postValue(chapter)
return true
}
return tryWithSuspend(true) { return tryWithSuspend(true) {
chapter.addImages( chapter.addImages(
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false

View file

@ -10,6 +10,9 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.LruCache import android.util.LruCache
import android.widget.Toast
import ani.dantotsu.logger
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -26,22 +29,23 @@ data class ImageData(
try { try {
// Fetch the image // Fetch the image
val response = httpSource.getImage(page) val response = httpSource.getImage(page)
println("Response: ${response.code}") logger("Response: ${response.code}")
println("Response: ${response.message}") logger("Response: ${response.message}")
// Convert the Response to an InputStream // Convert the Response to an InputStream
val inputStream = response.body?.byteStream() val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap // Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream) val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close() inputStream.close()
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100) //saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
return@withContext bitmap return@withContext bitmap
} catch (e: Exception) { } catch (e: Exception) {
// Handle any exceptions // Handle any exceptions
println("An error occurred: ${e.message}") logger("An error occurred: ${e.message}")
snackString("An error occurred: ${e.message}")
return@withContext null return@withContext null
} }
} }
@ -57,7 +61,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga") put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga")
} }
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
uri?.let { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
@ -65,7 +69,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
} }
} }
} else { } else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime") val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
if (!directory.exists()) { if (!directory.exists()) {
directory.mkdirs() directory.mkdirs()
} }

View file

@ -11,7 +11,7 @@ data class MangaChapter(
var link: String, var link: String,
var title: String? = null, var title: String? = null,
var description: String? = null, var description: String? = null,
var sChapter: SChapter var sChapter: SChapter,
) : Serializable { ) : Serializable {
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter) constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter)

View file

@ -4,6 +4,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemChapterListBinding import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -48,12 +49,71 @@ class MangaChapterAdapter(
} }
} }
private val activeDownloads = mutableSetOf<String>()
private val downloadedChapters = mutableSetOf<String>()
fun startDownload(chapterNumber: String) {
activeDownloads.add(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
fun stopDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.add(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
fun deleteDownload(chapterNumber: String) {
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) { inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(chapterNumber: String) {
if (activeDownloads.contains(chapterNumber)) {
// Show spinner
binding.itemDownload.setImageResource(R.drawable.spinner_icon_manga)
} else if(downloadedChapters.contains(chapterNumber)) {
// Show checkmark
binding.itemDownload.setImageResource(R.drawable.ic_check)
} else {
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_round_download_24)
}
}
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
} }
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val chapterNumber = arr[bindingAdapterPosition].number
if(activeDownloads.contains(chapterNumber)) {
fragment.onMangaChapterStopDownloadClick(chapterNumber)
return@setOnClickListener
}else if(downloadedChapters.contains(chapterNumber)) {
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
return@setOnClickListener
}else {
fragment.onMangaChapterDownloadClick(chapterNumber)
startDownload(chapterNumber)
}
}
}
} }
} }
@ -80,6 +140,7 @@ class MangaChapterAdapter(
is ChapterListViewHolder -> { is ChapterListViewHolder -> {
val binding = holder.binding val binding = holder.binding
val ep = arr[position] val ep = arr[position]
holder.bind(ep.number)
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemChapterNumber.text = ep.number binding.itemChapterNumber.text = ep.number
if (!ep.title.isNullOrEmpty()) { if (!ep.title.isNullOrEmpty()) {

View file

@ -2,6 +2,11 @@ package ani.dantotsu.media.manga
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -10,6 +15,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Toast import android.widget.Toast
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -20,10 +26,15 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
@ -41,8 +52,12 @@ import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -62,6 +77,8 @@ open class MangaReadFragment : Fragment() {
private lateinit var headerAdapter: MangaReadAdapter private lateinit var headerAdapter: MangaReadAdapter
private lateinit var chapterAdapter: MangaChapterAdapter private lateinit var chapterAdapter: MangaChapterAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f var screenWidth = 0f
private var progress = View.VISIBLE private var progress = View.VISIBLE
@ -81,6 +98,11 @@ open class MangaReadFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
}
requireContext().registerReceiver(downloadStatusReceiver, intentFilter)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
@ -132,6 +154,10 @@ open class MangaReadFragment : Fragment() {
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!) headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads){
chapterAdapter.stopDownload(download.chapter)
}
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -325,6 +351,57 @@ open class MangaReadFragment : Fragment() {
} }
} }
fun onMangaChapterDownloadClick(i: String) {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
// Fetch the image list and set it in the singleton
ServiceDataSingleton.imageData = parser.imageList("", chapter.sChapter)
// Now that imageData is set, start the service
ServiceDataSingleton.sourceMedia = media
val intent = Intent(context, MangaDownloaderService::class.java).apply {
putExtra("title", media.nameMAL)
putExtra("chapter", chapter.title)
}
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
ContextCompat.startForegroundService(requireContext(), intent)
}
}
}
}
}
fun onMangaChapterRemoveDownloadClick(i: String){
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
chapterAdapter.deleteDownload(i)
}
fun onMangaChapterStopDownloadClick(i: String) {
val intent = Intent(requireContext(), MangaDownloaderService::class.java)
requireContext().stopService(intent)
downloadManager.removeDownload(Download(media.nameMAL!!, i, Download.Type.MANGA))
chapterAdapter.deleteDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.startDownload(it) }
}
ACTION_DOWNLOAD_FINISHED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.stopDownload(it) }
}
}
}
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
@ -353,6 +430,7 @@ open class MangaReadFragment : Fragment() {
override fun onDestroy() { override fun onDestroy() {
model.mangaReadSources?.flushText() model.mangaReadSources?.flushText()
super.onDestroy() super.onDestroy()
requireContext().unregisterReceiver(downloadStatusReceiver)
} }
private var state: Parcelable? = null private var state: Parcelable? = null
@ -366,4 +444,10 @@ open class MangaReadFragment : Fragment() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
}
} }

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.net.Uri
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@ -29,6 +30,7 @@ import kotlinx.coroutines.withContext
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
abstract class BaseImageAdapter( abstract class BaseImageAdapter(
val activity: MangaReaderActivity, val activity: MangaReaderActivity,
@ -151,8 +153,9 @@ abstract class BaseImageAdapter(
Glide.with(this@loadBitmap) Glide.with(this@loadBitmap)
.asBitmap() .asBitmap()
.let { .let {
if (link.url.startsWith("file://")) { val fileUri = Uri.fromFile(File(link.url)).toString()
it.load(link.url) if (fileUri.startsWith("file://")) {
it.load(fileUri)
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {

View file

@ -47,7 +47,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
loaded = true loaded = true
binding.selectorAutoText.text = chp.title binding.selectorAutoText.text = chp.title
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if(model.loadMangaChapterImages(chp, m.selected!!)) { if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL!!)) {
val activity = currActivity() val activity = currActivity()
activity?.runOnUiThread { activity?.runOnUiThread {
tryWith { dismiss() } tryWith { dismiss() }

View file

@ -311,7 +311,7 @@ class MangaReaderActivity : AppCompatActivity() {
} }
} }
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) } scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!, media.nameMAL!!) }
} }
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
@ -700,6 +700,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages( model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!, chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!, media.selected!!,
media.nameMAL!!,
false false
) )
loading = false loading = false

View file

@ -3,6 +3,7 @@ package ani.dantotsu.parsers
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
@ -10,11 +11,14 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
@ -65,18 +69,24 @@ class AniyomiAdapter {
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val extension: AnimeExtension.Installed val extension: AnimeExtension.Installed
var sourceLanguage = 0 var sourceLanguage = 0
init { init {
this.extension = extension this.extension = extension
} }
override val name = extension.name override val name = extension.name
override val saveName = extension.name override val saveName = extension.name
override val hostUrl = extension.sources.first().name override val hostUrl = extension.sources.first().name
override val isDubAvailableSeparately = false override val isDubAvailableSeparately = false
override val isNSFW = extension.isNsfw override val isNSFW = extension.isNsfw
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> { override suspend fun loadEpisodes(
val source = try{ animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val source = try {
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
}catch (e: Exception){ } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} }
@ -97,7 +107,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
var incrementingNumber = 1f var incrementingNumber = 1f
sortedByStringNumber.map { sortedByStringNumber.map {
if (it.episode_number == Float.MAX_VALUE) { if (it.episode_number == Float.MAX_VALUE) {
it.episode_number = incrementingNumber++ // Update episode_number with the incrementing number it.episode_number =
incrementingNumber++ // Update episode_number with the incrementing number
} }
it it
} }
@ -117,10 +128,14 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
} }
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> { override suspend fun loadVideoServers(
val source = try{ episodeLink: String,
extra: Map<String, String>?,
sEpisode: SEpisode
): List<VideoServer> {
val source = try {
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
}catch (e: Exception){ } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList() } as? AnimeCatalogueSource ?: return emptyList()
@ -140,9 +155,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} }
override suspend fun search(query: String): List<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {
val source = try{ val source = try {
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
}catch (e: Exception){ } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? AnimeCatalogueSource ?: return emptyList() } as? AnimeCatalogueSource ?: return emptyList()
@ -152,7 +167,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} catch (e: CloudflareBypassException) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show() Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
.show()
} }
emptyList() emptyList()
} catch (e: Exception) { } catch (e: Exception) {
@ -186,9 +202,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
sEpisode.episode_number sEpisode.episode_number
} }
return Episode( return Episode(
if(episodeNumberInt.toInt() != -1){ if (episodeNumberInt.toInt() != -1) {
episodeNumberInt.toString() episodeNumberInt.toString()
}else{ } else {
sEpisode.name sEpisode.name
}, },
sEpisode.url, sEpisode.url,
@ -215,18 +231,24 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val mangaCache = Injekt.get<MangaCache>() val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed val extension: MangaExtension.Installed
var sourceLanguage = 0 var sourceLanguage = 0
init { init {
this.extension = extension this.extension = extension
} }
override val name = extension.name override val name = extension.name
override val saveName = extension.name override val saveName = extension.name
override val hostUrl = extension.sources.first().name override val hostUrl = extension.sources.first().name
override val isNSFW = extension.isNsfw override val isNSFW = extension.isNsfw
override suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?, sManga: SManga): List<MangaChapter> { override suspend fun loadChapters(
val source = try{ mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val source = try {
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
}catch (e: Exception){ } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList() } as? HttpSource ?: return emptyList()
@ -247,37 +269,79 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> { override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val source = try{ val source = try {
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
}catch (e: Exception){ } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList() } as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
return coroutineScope { val ret = coroutineScope {
try { try {
println("source.name " + source.name) println("source.name " + source.name)
val res = source.getPageList(sChapter) val res = source.getPageList(sChapter)
val reIndexedPages = res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) } val reIndexedPages =
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
val deferreds = reIndexedPages.map { page -> val deferreds = reIndexedPages.map { page ->
async(Dispatchers.IO) { async(Dispatchers.IO) {
mangaCache.put(page.imageUrl ?: "", ImageData(page, source)) mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
imageDataList += ImageData(page, source)
logger("put page: ${page.imageUrl}") logger("put page: ${page.imageUrl}")
pageToMangaImage(page) pageToMangaImage(page)
} }
} }
deferreds.awaitAll() deferreds.awaitAll()
} catch (e: Exception) { } catch (e: Exception) {
logger("loadImages Exception: $e") logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT).show() Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
.show()
emptyList() emptyList()
} }
} }
return ret
} }
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? { suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData>{
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
var imageDataList: List<ImageData> = listOf()
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) {
imageDataList += ImageData(page, source)
}
}
deferreds.awaitAll()
} catch (e: Exception) {
logger("loadImages Exception: $e")
Toast.makeText(currContext(), "Failed to load images: $e", Toast.LENGTH_SHORT)
.show()
emptyList()
}
}
return imageDataList
}
suspend fun fetchAndProcessImage(
page: Page,
httpSource: HttpSource,
context: Context
): Bitmap? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
// Fetch the image // Fetch the image
@ -310,7 +374,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) { fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
@ -325,7 +388,13 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// Save the Bitmap using MediaStore API // Save the Bitmap using MediaStore API
saveImage(bitmap, contentResolver, "image_${System.currentTimeMillis()}.jpg", Bitmap.CompressFormat.JPEG, 100) saveImage(
bitmap,
contentResolver,
"image_${System.currentTimeMillis()}.jpg",
Bitmap.CompressFormat.JPEG,
100
)
} }
inputStream?.close() inputStream?.close()
@ -336,16 +405,28 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
} }
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) { fun saveImage(
bitmap: Bitmap,
contentResolver: ContentResolver,
filename: String,
format: Bitmap.CompressFormat,
quality: Int
) {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename) put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime") put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime"
)
} }
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) val uri: Uri? = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
uri?.let { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
@ -353,7 +434,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
} }
} else { } else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime") val directory =
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
if (!directory.exists()) { if (!directory.exists()) {
directory.mkdirs() directory.mkdirs()
} }
@ -370,11 +452,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
override suspend fun search(query: String): List<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {
val source = try{ val source = try {
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
}catch (e: Exception){ } catch (e: Exception) {
sourceLanguage = 0 sourceLanguage = 0
extension.sources[sourceLanguage] extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList() } as? HttpSource ?: return emptyList()
@ -386,7 +467,8 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} catch (e: CloudflareBypassException) { } catch (e: CloudflareBypassException) {
logger("Exception in search: $e") logger("Exception in search: $e")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT).show() Toast.makeText(currContext(), "Failed to bypass Cloudflare", Toast.LENGTH_SHORT)
.show()
} }
emptyList() emptyList()
} catch (e: Exception) { } catch (e: Exception) {
@ -468,8 +550,10 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
} }
fun parseChapterTitle(title: String): Triple<String?, String?, String> { fun parseChapterTitle(title: String): Triple<String?, String?, String> {
val volumePattern = Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE) val volumePattern =
val chapterPattern = Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE) 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 volumeMatcher = volumePattern.matcher(title)
val chapterMatcher = chapterPattern.matcher(title) val chapterMatcher = chapterPattern.matcher(title)
@ -479,10 +563,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
var remainingTitle = title var remainingTitle = title
if (volumeNumber != null) { if (volumeNumber != null) {
remainingTitle = volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString() remainingTitle =
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
} }
if (chapterNumber != null) { if (chapterNumber != null) {
remainingTitle = chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString() remainingTitle =
chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
} }
return Triple(volumeNumber, chapterNumber, remainingTitle.trim()) return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
@ -505,7 +591,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: eu.kanade.tachiyomi.animesource.model.Video): ani.dantotsu.parsers.Video {
// Find the number value from the .quality string // Find the number value from the .quality string
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0 val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
@ -537,7 +623,8 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
logger("Unknown video format: $videoUrl") logger("Unknown video format: $videoUrl")
throw Exception("Unknown video format") throw Exception("Unknown video format")
} }
val headersMap: Map<String, String> = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf() val headersMap: Map<String, String> =
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
return ani.dantotsu.parsers.Video( return ani.dantotsu.parsers.Video(
@ -550,7 +637,11 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
private fun getVideoType(fileName: String): VideoType? { private fun getVideoType(fileName: String): VideoType? {
return when { return when {
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
".mkv",
ignoreCase = true
) -> VideoType.CONTAINER
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8 fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
else -> VideoType.CONTAINER else -> VideoType.CONTAINER
@ -563,7 +654,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
runBlocking { runBlocking {
type = findSubtitleType(track.url) type = findSubtitleType(track.url)
} }
return Subtitle(track.lang, track.url, type?: SubtitleType.SRT) return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
} }
private fun findSubtitleType(url: String): SubtitleType? { private fun findSubtitleType(url: String): SubtitleType? {

View file

@ -81,8 +81,8 @@ data class MangaImage(
val useTransformation: Boolean = false, val useTransformation: Boolean = false,
val page: Page val page: Page? = null,
) : Serializable{ ) : Serializable{
constructor(url: String,useTransformation: Boolean=false, page: Page) constructor(url: String,useTransformation: Boolean=false, page: Page? = null)
: this(FileUrl(url),useTransformation, page) : this(FileUrl(url),useTransformation, page)
} }

View file

@ -174,7 +174,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT)
true true
} }
val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan") val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan", "Libre")
binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false) binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false)
binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns)) binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns))
binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ -> binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ ->

View file

@ -21,6 +21,7 @@ const val PREF_DOH_MULLVAD = 9
const val PREF_DOH_CONTROLD = 10 const val PREF_DOH_CONTROLD = 10
const val PREF_DOH_NJALLA = 11 const val PREF_DOH_NJALLA = 11
const val PREF_DOH_SHECAN = 12 const val PREF_DOH_SHECAN = 12
const val PREF_DOH_LIBREDNS = 13
fun OkHttpClient.Builder.dohCloudflare() = dns( fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build()) DnsOverHttps.Builder().client(build())
@ -184,3 +185,13 @@ fun OkHttpClient.Builder.dohShecan() = dns(
) )
.build(), .build(),
) )
fun OkHttpClient.Builder.dohLibreDNS() = dns(
DnsOverHttps.Builder().client(build())
.url("https://doh.libredns.gr/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("116.202.176.26"), // IPv4 address for LibreDNS
InetAddress.getByName("192.71.166.92") // Fallback IPv4 address
)
.build()
)

View file

@ -62,6 +62,7 @@ class NetworkHelper(
PREF_DOH_CONTROLD -> builder.dohControlD() PREF_DOH_CONTROLD -> builder.dohControlD()
PREF_DOH_NJALLA -> builder.dohNajalla() PREF_DOH_NJALLA -> builder.dohNajalla()
PREF_DOH_SHECAN -> builder.dohShecan() PREF_DOH_SHECAN -> builder.dohShecan()
PREF_DOH_LIBREDNS -> builder.dohLibreDNS()
} }
return builder return builder

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M382,720 L154,492l57,-57 171,171 367,-367 57,57 -424,424Z"/>
</vector>

View file

@ -70,6 +70,15 @@
</LinearLayout> </LinearLayout>
<ImageView
android:id="@+id/itemDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="8dp"
app:srcCompat="@drawable/ic_round_download_24"
tools:ignore="ContentDescription" />
<View <View
android:id="@+id/itemEpisodeViewedCover" android:id="@+id/itemEpisodeViewedCover"
android:layout_width="match_parent" android:layout_width="match_parent"