diff --git a/README.md b/README.md index a1b1d673..6b713a2a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Dantotsu is an [Anilist](https://anilist.co/) only client. > **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge! + + ## Terms of Use By downloading, installing, or using this application, you agree to: - Use the application in compliance with all applicable laws diff --git a/app/build.gradle b/app/build.gradle index 535d8922..bcda959a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { minSdk 21 targetSdk 35 versionCode((System.currentTimeMillis() / 60000).toInteger()) - versionName "3.2.2" - versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer + versionName "3.2.1" + versionCode 300200100 signingConfig signingConfigs.debug } @@ -48,10 +48,6 @@ android { manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round" debuggable System.getenv("CI") == null isDefault true - debuggable true - jniDebuggable true - minifyEnabled false - shrinkResources false } debug { applicationIdSuffix ".beta" @@ -85,26 +81,25 @@ android { dependencies { // FireBase - googleImplementation platform('com.google.firebase:firebase-bom:33.13.0') - googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.4.0' - googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.4.3' + googleImplementation platform('com.google.firebase:firebase-bom:33.0.0') + googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0' + googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0' // Core - implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.browser:browser:1.8.0' - implementation 'androidx.core:core-ktx:1.16.0' - implementation 'androidx.fragment:fragment-ktx:1.8.6' - implementation 'androidx.activity:activity-ktx:1.10.1' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.multidex:multidex:2.0.1' - implementation "androidx.work:work-runtime-ktx:2.10.1" + implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.github.Blatzar:NiceHttp:0.4.4' - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' implementation 'androidx.preference:preference-ktx:1.2.1' - implementation 'androidx.webkit:webkit:1.13.0' + implementation 'androidx.webkit:webkit:1.11.0' implementation "com.anggrayudi:storage:1.5.5" implementation "androidx.biometric:biometric:1.1.0" @@ -118,7 +113,7 @@ dependencies { implementation 'jp.wasabeef:glide-transformations:4.3.0' // Exoplayer - ext.exo_version = '1.6.1' + ext.exo_version = '1.5.1' implementation "androidx.media3:media3-exoplayer:$exo_version" implementation "androidx.media3:media3-ui:$exo_version" implementation "androidx.media3:media3-exoplayer-hls:$exo_version" @@ -138,7 +133,7 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.github.VipulOG:ebook-reader:0.1.6' - implementation 'androidx.paging:paging-runtime-ktx:3.3.6' + implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'com.github.eltos:simpledialogfragments:v3.7' implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3' @@ -167,13 +162,13 @@ dependencies { implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07' implementation 'com.squareup.logcat:logcat:0.1' implementation 'uy.kohesive.injekt:injekt-core:1.16.+' - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' - implementation 'com.squareup.okio:okio:3.9.1' - implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.14' - implementation 'org.jsoup:jsoup:1.18.1' - implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.7.3' + implementation 'com.squareup.okio:okio:3.8.0' + implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12' + implementation 'org.jsoup:jsoup:1.16.1' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.github.tachiyomiorg:unifile:17bec43' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt index 0b5751e0..057c6d22 100644 --- a/app/src/main/java/ani/dantotsu/App.kt +++ b/app/src/main/java/ani/dantotsu/App.kt @@ -113,28 +113,21 @@ class App : MultiDexApplication() { } } - val scope = CoroutineScope(Dispatchers.IO) - scope.launch { + CoroutineScope(Dispatchers.IO).launch { animeExtensionManager = Injekt.get() - launch { - animeExtensionManager.findAvailableExtensions() - } + animeExtensionManager.findAvailableExtensions() Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}") AnimeSources.init(animeExtensionManager.installedExtensionsFlow) } - scope.launch { + CoroutineScope(Dispatchers.IO).launch { mangaExtensionManager = Injekt.get() - launch { - mangaExtensionManager.findAvailableExtensions() - } + mangaExtensionManager.findAvailableExtensions() Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") MangaSources.init(mangaExtensionManager.installedExtensionsFlow) } - scope.launch { + CoroutineScope(Dispatchers.IO).launch { novelExtensionManager = Injekt.get() - launch { - novelExtensionManager.findAvailableExtensions() - } + novelExtensionManager.findAvailableExtensions() Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") NovelSources.init(novelExtensionManager.installedExtensionsFlow) } diff --git a/app/src/main/java/ani/dantotsu/Network.kt b/app/src/main/java/ani/dantotsu/Network.kt index f9d8b7f2..ce536e8d 100644 --- a/app/src/main/java/ani/dantotsu/Network.kt +++ b/app/src/main/java/ani/dantotsu/Network.kt @@ -11,11 +11,7 @@ import com.lagradost.nicehttp.addGenericDns import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi @@ -89,42 +85,12 @@ object Mapper : ResponseParser { } } -/** - * Performs parallel processing of collection items without blocking threads. - * Each operation runs in its own coroutine on the specified dispatcher. - * - * @param dispatcher The CoroutineDispatcher to use for parallel operations (defaults to IO) - * @param f The suspend function to apply to each item - * @return List of results in the same order as the original collection - */ -suspend fun Collection.asyncMap( - dispatcher: CoroutineDispatcher = Dispatchers.IO, - f: suspend (A) -> B -): List = coroutineScope { - map { item -> - async(dispatcher) { - f(item) - } - }.awaitAll() +fun Collection.asyncMap(f: suspend (A) -> B): List = runBlocking { + map { async { f(it) } }.map { it.await() } } -/** - * Performs parallel processing of collection items without blocking threads, - * filtering out null results. - * - * @param dispatcher The CoroutineDispatcher to use for parallel operations (defaults to IO) - * @param f The suspend function to apply to each item - * @return List of non-null results in the same order as the original collection - */ -suspend fun Collection.asyncMapNotNull( - dispatcher: CoroutineDispatcher = Dispatchers.IO, - f: suspend (A) -> B? -): List = coroutineScope { - map { item -> - async(dispatcher) { - f(item) - } - }.mapNotNull { it.await() } +fun Collection.asyncMapNotNull(f: suspend (A) -> B?): List = runBlocking { + map { async { f(it) } }.mapNotNull { it.await() } } fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) { diff --git a/app/src/main/java/ani/dantotsu/connections/discord/Login.kt b/app/src/main/java/ani/dantotsu/connections/discord/Login.kt index f33c03e9..513da55b 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/Login.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/Login.kt @@ -47,9 +47,9 @@ class Login : AppCompatActivity() { view.evaluateJavascript( """ (function() { - const m = []; webpackChunkdiscord_app.push([[""], {}, e => {for (let c in e.c)m.push(e.c[c])}]); - return m.find(n => n?.exports?.default?.getToken !== void 0)?.exports?.default?.getToken(); -})() + const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken(); + return wreq; + })() """.trimIndent() ) { result -> login(result.trim('"')) diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt index 1e1d2e19..bcb21c8c 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt @@ -4,7 +4,6 @@ import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Intent import android.content.res.Configuration -import android.graphics.Color import android.os.Bundle import android.text.SpannableStringBuilder import android.view.GestureDetector @@ -13,8 +12,6 @@ import android.view.View import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import android.widget.ImageView -import androidx.activity.SystemBarStyle -import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -22,10 +19,8 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.text.bold import androidx.core.text.color -import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.isVisible import androidx.core.view.marginBottom -import androidx.core.view.setPadding import androidx.core.view.updateLayoutParams import androidx.core.view.updateMargins import androidx.fragment.app.Fragment @@ -84,7 +79,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia() val id = intent.getIntExtra("mediaId", -1) @@ -115,7 +109,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi // Ui init initActivity(this) - binding.mediaViewPager.updateLayoutParams { bottomMargin = navBarHeight } @@ -139,12 +132,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi val navBarBottomMargin = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE ) 0 else navBarHeight - binding.mediaBottomBarContainer.setPadding( - navBar.paddingLeft, - navBar.paddingTop, - navBar.paddingRight + navBarRightMargin, - navBar.paddingBottom + navBarBottomMargin - ) + navBar.updateLayoutParams { + rightMargin = navBarRightMargin + bottomMargin = navBarBottomMargin + } binding.mediaBanner.updateLayoutParams { height += statusBarHeight } binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight } binding.mediaClose.updateLayoutParams { topMargin += statusBarHeight } diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 7fa32de9..5c66f6a2 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -1,8 +1,6 @@ package ani.dantotsu.media import android.content.Context -import androidx.core.net.toFile -import androidx.core.net.toUri import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.parsers.SubtitleType @@ -23,32 +21,28 @@ class SubtitleDownloader { suspend fun loadSubtitleType(url: String): SubtitleType = withContext(Dispatchers.IO) { return@withContext try { - if (!url.startsWith("file")) { - // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it - val networkHelper = Injekt.get() - val request = Request.Builder() - .url(url) - .build() + // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it + val networkHelper = Injekt.get() + val request = Request.Builder() + .url(url) + .build() - val response = networkHelper.client.newCall(request).execute() + val response = networkHelper.client.newCall(request).execute() - // Check if response is successful - if (response.isSuccessful) { - val responseBody = response.body.string() + // Check if response is successful + if (response.isSuccessful) { + val responseBody = response.body.string() - val subtitleType = getType(responseBody) - - subtitleType - } else { - SubtitleType.UNKNOWN + val subtitleType = when { + responseBody.contains("[Script Info]") -> SubtitleType.ASS + responseBody.contains("WEBVTT") -> SubtitleType.VTT + else -> SubtitleType.SRT } - } else { - val uri = url.toUri() - val file = uri.toFile() - val fileBody = file.readText() - val subtitleType = getType(fileBody) + subtitleType + } else { + SubtitleType.UNKNOWN } } catch (e: Exception) { Logger.log(e) @@ -56,15 +50,6 @@ class SubtitleDownloader { } } - private fun getType(content: String): SubtitleType { - return when { - content.contains("[Script Info]") -> SubtitleType.ASS - content.contains("WEBVTT") -> SubtitleType.VTT - content.contains("SRT") -> SubtitleType.SRT - else -> SubtitleType.UNKNOWN - } - } - //actually downloads lol @Deprecated("handled externally") suspend fun downloadSubtitle( diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index 75332806..465949a4 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -2,6 +2,7 @@ package ani.dantotsu.media.anime import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.app.AlertDialog import android.app.Dialog import android.app.PictureInPictureParams import android.app.PictureInPictureUiState @@ -18,6 +19,7 @@ import android.media.AudioManager.AUDIOFOCUS_GAIN import android.media.AudioManager.AUDIOFOCUS_LOSS import android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT import android.media.AudioManager.STREAM_MUSIC +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.CountDownTimer @@ -73,7 +75,9 @@ import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.text.CueGroup import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultLoadControl @@ -171,10 +175,10 @@ import java.util.Timer import java.util.TimerTask import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import kotlin.collections.set import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt -import androidx.core.net.toUri @UnstableApi @SuppressLint("ClickableViewAccessibility") @@ -423,8 +427,7 @@ class ExoplayerView : false -> 0f } - val textElevation = - PrefManager.getVal(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels + val textElevation = PrefManager.getVal(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels textView.translationY = -textElevation } @@ -603,9 +606,9 @@ class ExoplayerView : if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { pipEnabled = packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && - PrefManager.getVal( - PrefName.Pip, - ) + PrefManager.getVal( + PrefName.Pip, + ) if (pipEnabled) { exoPip.visibility = View.VISIBLE exoPip.setOnClickListener { @@ -1041,8 +1044,7 @@ class ExoplayerView : } } - override fun onSingleClick(event: MotionEvent) = - if (isSeeking) doubleTap(false, event) else handleController() + override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(false, event) else handleController() }, ) val rewindArea = playerView.findViewById(R.id.exo_rewind_area) @@ -1077,8 +1079,7 @@ class ExoplayerView : } } - override fun onSingleClick(event: MotionEvent) = - if (isSeeking) doubleTap(true, event) else handleController() + override fun onSingleClick(event: MotionEvent) = if (isSeeking) doubleTap(true, event) else handleController() }, ) val forwardArea = playerView.findViewById(R.id.exo_forward_area) @@ -1308,7 +1309,7 @@ class ExoplayerView : setTitle(R.string.speed) singleChoiceItems(speedsName, curSpeed) { i -> PrefManager.setCustomVal("${media.id}_speed", i) - speed = speeds.getOrNull(i) ?: 1f + speed = speeds[i] curSpeed = i playbackParameters = PlaybackParameters(speed) exoPlayer.playbackParameters = playbackParameters @@ -1358,7 +1359,7 @@ class ExoplayerView : val showProgressDialog = if (PrefManager.getVal(PrefName.AskIndividualPlayer)) { PrefManager.getCustomVal( - "${media.id}_progressDialog", + "${media.id}_ProgressDialog", true, ) } else { @@ -1380,7 +1381,7 @@ class ExoplayerView : setCancelable(false) setPosButton(R.string.yes) { PrefManager.setCustomVal( - "${media.id}_progressDialog", + "${media.id}_ProgressDialog", false, ) PrefManager.setCustomVal( @@ -1391,7 +1392,7 @@ class ExoplayerView : } setNegButton(R.string.no) { PrefManager.setCustomVal( - "${media.id}_progressDialog", + "${media.id}_ProgressDialog", false, ) PrefManager.setCustomVal( @@ -1448,8 +1449,7 @@ class ExoplayerView : else -> mutableListOf() } val startTimestamp = Calendar.getInstance() - val durationInSeconds = - if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440 + val durationInSeconds = if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440 val endTimestamp = Calendar.getInstance().apply { @@ -1502,12 +1502,12 @@ class ExoplayerView : @Suppress("UNCHECKED_CAST") val list = ( - PrefManager.getNullableCustomVal( - "continueAnimeList", - listOf(), - List::class.java, - ) as List - ).toMutableList() + PrefManager.getNullableCustomVal( + "continueAnimeList", + listOf(), + List::class.java, + ) as List + ).toMutableList() if (list.contains(media.id)) list.remove(media.id) list.add(media.id) PrefManager.setCustomVal("continueAnimeList", list) @@ -1567,11 +1567,7 @@ class ExoplayerView : subtitle = intent.getSerialized("subtitle") ?: when ( val subLang: String? = - PrefManager.getNullableCustomVal( - "subLang_${media.id}", - null, - String::class.java - ) + PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java) ) { null -> { when (episode.selectedSubtitle) { @@ -1579,12 +1575,8 @@ class ExoplayerView : -1 -> ext.subtitles.find { it.language.contains(lang, ignoreCase = true) || - it.language.contains( - getLanguageCode(lang), - ignoreCase = true - ) + it.language.contains(getLanguageCode(lang), ignoreCase = true) } - else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!) } } @@ -1605,27 +1597,29 @@ class ExoplayerView : emptyList().toMutableList() ext.subtitles.forEach { subtitle -> val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle.file.url + // var localFile: String? = null if (subtitle.type == SubtitleType.UNKNOWN) { runBlocking { val type = SubtitleDownloader.loadSubtitleType(subtitleUrl) - val fileUri = (subtitleUrl).toUri() + val fileUri = Uri.parse(subtitleUrl) sub += MediaItem.SubtitleConfiguration .Builder(fileUri) .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) .setMimeType( when (type) { - SubtitleType.VTT -> MimeTypes.TEXT_VTT + SubtitleType.VTT -> MimeTypes.TEXT_SSA SubtitleType.ASS -> MimeTypes.TEXT_SSA - SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP - else -> MimeTypes.TEXT_UNKNOWN + SubtitleType.SRT -> MimeTypes.TEXT_SSA + else -> MimeTypes.TEXT_SSA }, ).setId("69") .setLanguage(subtitle.language) .build() } + println("sub: $sub") } else { - val subUri = subtitleUrl.toUri() + val subUri = Uri.parse(subtitleUrl) sub += MediaItem.SubtitleConfiguration .Builder(subUri) @@ -1655,18 +1649,26 @@ class ExoplayerView : followRedirects(true) followSslRedirects(true) }.build() - val httpDataSourceFactory = - OkHttpDataSource.Factory(httpClient).apply { - setDefaultRequestProperties(defaultHeaders) - video?.file?.headers?.let { - setDefaultRequestProperties(it) + val dataSourceFactory = + DataSource.Factory { + val dataSource: HttpDataSource = OkHttpDataSource.Factory(httpClient).createDataSource() + defaultHeaders.forEach { + dataSource.setRequestProperty(it.key, it.value) } + video?.file?.headers?.forEach { + dataSource.setRequestProperty(it.key, it.value) + } + dataSource } - val defaultDataSourceFactory = DefaultDataSource.Factory(this, httpDataSourceFactory) + val dafuckDataSourceFactory = DefaultDataSource.Factory(this) cacheFactory = CacheDataSource.Factory().apply { setCache(VideoCache.getInstance(this@ExoplayerView)) - setUpstreamDataSourceFactory(defaultDataSourceFactory) + if (ext.server.offline) { + setUpstreamDataSourceFactory(dafuckDataSourceFactory) + } else { + setUpstreamDataSourceFactory(dataSourceFactory) + } setCacheWriteDataSinkFactory(null) } @@ -1715,18 +1717,16 @@ class ExoplayerView : val docFile = directory.listFiles().firstOrNull { it.name?.endsWith(".mp4") == true || - it.name?.endsWith(".mkv") == true || - it.name?.endsWith( - ".${ - Injekt - .get() - .extension - ?.extension - ?.getFileExtension() - ?.first ?: "ts" - }", - ) == - true + it.name?.endsWith(".mkv") == true || + it.name?.endsWith( + ".${Injekt + .get() + .extension + ?.extension + ?.getFileExtension() + ?.first ?: "ts"}", + ) == + true } if (docFile != null) { val uri = docFile.uri @@ -1840,30 +1840,30 @@ class ExoplayerView : "%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(playbackPosition), TimeUnit.MILLISECONDS.toMinutes(playbackPosition) - - TimeUnit.HOURS.toMinutes( - TimeUnit.MILLISECONDS.toHours( - playbackPosition, - ), + TimeUnit.HOURS.toMinutes( + TimeUnit.MILLISECONDS.toHours( + playbackPosition, ), + ), TimeUnit.MILLISECONDS.toSeconds(playbackPosition) - - TimeUnit.MINUTES.toSeconds( - TimeUnit.MILLISECONDS.toMinutes( - playbackPosition, - ), + TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes( + playbackPosition, ), + ), ) - customAlertDialog().apply { - setTitle(getString(R.string.continue_from, time)) - setCancelable(false) - setPosButton(getString(R.string.yes)) { - buildExoplayer() + customAlertDialog().apply { + setTitle(getString(R.string.continue_from, time)) + setCancelable(false) + setPosButton(getString(R.string.yes)) { + buildExoplayer() + } + setNegButton(getString(R.string.no)) { + playbackPosition = 0L + buildExoplayer() + } + show() } - setNegButton(getString(R.string.no)) { - playbackPosition = 0L - buildExoplayer() - } - show() - } } else { buildExoplayer() } @@ -1940,9 +1940,7 @@ class ExoplayerView : val currentPosition = exoPlayer.currentPosition - if ((lastSubtitle?.length - ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500) - ) { + if ((lastSubtitle?.length ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)) { activeSubtitles.clear() } @@ -2189,7 +2187,7 @@ class ExoplayerView : currentTimeStamp = model.timeStamps.value?.find { timestamp -> timestamp.interval.startTime < playerCurrentTime && - playerCurrentTime < (timestamp.interval.endTime - 1) + playerCurrentTime < (timestamp.interval.endTime - 1) } val new = currentTimeStamp @@ -2215,8 +2213,7 @@ class ExoplayerView : override fun onTick(millisUntilFinished: Long) { if (new == null) { skipTimeButton.visibility = View.GONE - exoSkip.isVisible = - PrefManager.getVal(PrefName.SkipTime) > 0 + exoSkip.isVisible = PrefManager.getVal(PrefName.SkipTime) > 0 disappeared = false functionstarted = false cancelTimer() @@ -2225,8 +2222,7 @@ class ExoplayerView : override fun onFinish() { skipTimeButton.visibility = View.GONE - exoSkip.isVisible = - PrefManager.getVal(PrefName.SkipTime) > 0 + exoSkip.isVisible = PrefManager.getVal(PrefName.SkipTime) > 0 disappeared = true functionstarted = false cancelTimer() @@ -2314,7 +2310,7 @@ class ExoplayerView : tracks.groups.forEach { println( "Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" + - "Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}", + "Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}", ) when (it.type) { TRACK_TYPE_AUDIO -> { @@ -2369,7 +2365,7 @@ class ExoplayerView : when (error.errorCode) { PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, - -> { + -> { toast("Source Exception : ${error.message}") isPlayerPlaying = true sourceClick() @@ -2407,9 +2403,9 @@ class ExoplayerView : val incognito: Boolean = PrefManager.getVal(PrefName.Incognito) val episodeEnd = exoPlayer.currentPosition / episodeLength > - PrefManager.getVal( - PrefName.WatchPercentage, - ) + PrefManager.getVal( + PrefName.WatchPercentage, + ) val episode0 = currentEpisodeIndex == 0 && PrefManager.getVal(PrefName.ChapterZeroPlayer) if (!incognito && (episodeEnd || episode0) && Anilist.userid != null ) { @@ -2488,7 +2484,7 @@ class ExoplayerView : val videoURL = video?.file?.url ?: return val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle!!.file.url val shareVideo = Intent(Intent.ACTION_VIEW) - shareVideo.setDataAndType(videoURL.toUri(), "video/*") + shareVideo.setDataAndType(Uri.parse(videoURL), "video/*") shareVideo.setPackage("com.instantbits.cast.webvideo") if (subtitle != null) shareVideo.putExtra("subtitle", subtitleUrl) shareVideo.putExtra( @@ -2510,7 +2506,7 @@ class ExoplayerView : } catch (ex: ActivityNotFoundException) { val intent = Intent(Intent.ACTION_VIEW) val uriString = "market://details?id=com.instantbits.cast.webvideo" - intent.data = uriString.toUri() + intent.data = Uri.parse(uriString) startActivity(intent) } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt index f47f416f..359c64a5 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt @@ -16,7 +16,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -import java.io.FileNotFoundException import java.io.FileOutputStream data class ImageData( @@ -77,7 +76,7 @@ fun saveImage( uri?.let { contentResolver.openOutputStream(it)?.use { os -> bitmap.compress(format, quality, os) - } ?: throw FileNotFoundException("Failed to open output stream for URI: $uri") + } } } else { val directory = @@ -87,20 +86,12 @@ fun saveImage( } val file = File(directory, filename) - - // Check if the file already exists - if (file.exists()) { - println("File already exists: ${file.absolutePath}") - return - } - FileOutputStream(file).use { outputStream -> bitmap.compress(format, quality, outputStream) } } - } catch (e: FileNotFoundException) { - println("File not found: ${e.message}") } catch (e: Exception) { + // Handle exception here println("Exception while saving image: ${e.message}") } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index 2ad28a75..63ef7408 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -66,7 +66,6 @@ import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.source.ConfigurableSource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt @@ -233,35 +232,25 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } fun multiDownload(n: Int) { - lifecycleScope.launch { - // Get the last viewed chapter - val selected = media.userProgress ?: 0 - val chapters = media.manga?.chapters?.values?.toList() - // Ensure chapters are available in the extensions - if (chapters.isNullOrEmpty() || n < 1) return@launch - // Find the index of the last viewed chapter - val progressChapterIndex = (chapters.indexOfFirst { - MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected - } + 1).coerceAtLeast(0) - // Calculate the end value for the range of chapters to download - val endIndex = (progressChapterIndex + n).coerceAtMost(chapters.size) - // Get the list of chapters to download - val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex) - // Trigger the download for each chapter sequentially - for (chapter in chaptersToDownload) { - try { - downloadChapterSequentially(chapter) - } catch (e: Exception) { - Toast.makeText(requireContext(), "Failed to download chapter: ${chapter.title}", Toast.LENGTH_SHORT).show() - } - } - Toast.makeText(requireContext(), "All downloads completed!", Toast.LENGTH_SHORT).show() - } - } - private suspend fun downloadChapterSequentially(chapter: MangaChapter) { - withContext(Dispatchers.IO) { + // Get last viewed chapter + val selected = media.userProgress + val chapters = media.manga?.chapters?.values?.toList() + // Filter by selected language + val progressChapterIndex = (chapters?.indexOfFirst { + MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected + } ?: 0) + 1 + + if (progressChapterIndex < 0 || n < 1 || chapters == null) return + + // Calculate the end index + val endIndex = minOf(progressChapterIndex + n, chapters.size) + + // Make sure there are enough chapters + val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex) + + + for (chapter in chaptersToDownload) { onMangaChapterDownloadClick(chapter) - delay(2000) // A 2-second download } } diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt index f534ce6b..f343d70b 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt @@ -26,6 +26,7 @@ object AnimeSources : WatchSources() { ) isInitialized = true + // Update as StateFlow emits new values fromExtensions.collect { extensions -> list = sortPinnedAnimeSources( createParsersFromExtensions(extensions), diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 0f2a438a..adfd857e 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -226,18 +226,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { ?: return emptyList()) return try { - // TODO(1.6): Remove else block when dropping support for ext lib <1.6 - if ((source as AnimeHttpSource).javaClass.declaredMethods.any { it.name == "getHosterList" }){ - val hosters = source.getHosterList(sEpisode) - val allVideos = hosters.flatMap { hoster -> - val videos = source.getVideoList(hoster) - videos.map { it.copy(videoTitle = "${hoster.hosterName} - ${it.videoTitle}") } - } - allVideos.map { videoToVideoServer(it) } - } else { - val videos = source.getVideoList(sEpisode) - videos.map { videoToVideoServer(it) } - } + val videos = source.getVideoList(sEpisode) + videos.map { videoToVideoServer(it) } } catch (e: Exception) { Logger.log("Exception occurred: ${e.message}") emptyList() @@ -586,7 +576,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac number, format!!, FileUrl(videoUrl, headersMap), - null + if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble() ) } @@ -646,6 +636,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac } private fun trackToSubtitle(track: Track): Subtitle { + //use Dispatchers.IO to make a HTTP request to determine the subtitle type var type: SubtitleType? runBlocking { type = findSubtitleType(track.url) diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt index 4b54360a..171b07b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/AnimeSource.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.animesource -import eu.kanade.tachiyomi.animesource.model.Hoster import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.Video @@ -49,25 +48,6 @@ interface AnimeSource { return fetchEpisodeList(anime).awaitSingle() } - /** - * Get the list of hoster for an episode. The first hoster in the list should - * be the preferred hoster. - * - * @since extensions-lib 16 - * @param episode the episode. - * @return the hosters for the episode. - */ - suspend fun getHosterList(episode: SEpisode): List = throw IllegalStateException("Not used") - - /** - * Get the list of videos for a hoster. - * - * @since extensions-lib 16 - * @param hoster the hoster. - * @return the videos for the hoster. - */ - suspend fun getVideoList(hoster: Hoster): List