rough outline for downloading anime
This commit is contained in:
parent
42c3b42c05
commit
c9649751d2
11 changed files with 643 additions and 26 deletions
|
@ -274,8 +274,9 @@
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
<service
|
<service
|
||||||
android:name=".download.video.MyDownloadService"
|
android:name=".download.video.MyDownloadService"
|
||||||
android:exported="false">
|
android:exported="false"
|
||||||
<intent-filter>
|
android:foregroundServiceType="dataSync">
|
||||||
|
<intent-filter>
|
||||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
@ -297,6 +298,9 @@
|
||||||
android:name=".download.novel.NovelDownloaderService"
|
android:name=".download.novel.NovelDownloaderService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
|
<service android:name=".download.anime.AnimeDownloaderService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
<service
|
<service
|
||||||
android:name=".connections.discord.DiscordService"
|
android:name=".connections.discord.DiscordService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
@ -11,12 +11,14 @@ import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnticipateInterpolator
|
import android.view.animation.AnticipateInterpolator
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -26,11 +28,13 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||||
import ani.dantotsu.databinding.ActivityMainBinding
|
import ani.dantotsu.databinding.ActivityMainBinding
|
||||||
import ani.dantotsu.databinding.SplashScreenBinding
|
import ani.dantotsu.databinding.SplashScreenBinding
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.home.AnimeFragment
|
import ani.dantotsu.home.AnimeFragment
|
||||||
import ani.dantotsu.home.HomeFragment
|
import ani.dantotsu.home.HomeFragment
|
||||||
import ani.dantotsu.home.LoginFragment
|
import ani.dantotsu.home.LoginFragment
|
||||||
|
@ -45,6 +49,7 @@ import ani.dantotsu.themes.ThemeManager
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -60,7 +65,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
private var uiSettings = UserInterfaceSettings()
|
private var uiSettings = UserInterfaceSettings()
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
@OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
LangSet.setLocale(this)
|
LangSet.setLocale(this)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -242,6 +247,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||||
|
if (index != null) {
|
||||||
|
val downloadCursor = index.getDownloads()
|
||||||
|
if (downloadCursor != null) {
|
||||||
|
while (downloadCursor.moveToNext()) {
|
||||||
|
val download = downloadCursor.download
|
||||||
|
Log.e("Downloader", download.request.uri.toString())
|
||||||
|
Log.e("Downloader", download.request.id.toString())
|
||||||
|
Log.e("Downloader", download.request.mimeType.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@ package ani.dantotsu.aniyomi.anime.custom
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
|
@ -27,7 +31,7 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
@OptIn(UnstableApi::class) override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
addSingletonFactory { DownloadsManager(app) }
|
addSingletonFactory { DownloadsManager(app) }
|
||||||
|
@ -51,6 +55,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||||
|
|
||||||
addSingletonFactory { MangaCache() }
|
addSingletonFactory { MangaCache() }
|
||||||
|
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
|
|
|
@ -0,0 +1,420 @@
|
||||||
|
package ani.dantotsu.download.anime
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.currActivity
|
||||||
|
import ani.dantotsu.download.Download
|
||||||
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
|
import ani.dantotsu.download.video.MyDownloadService
|
||||||
|
import ani.dantotsu.logger
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
|
import ani.dantotsu.parsers.Subtitle
|
||||||
|
import ani.dantotsu.parsers.Video
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.InstanceCreator
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.Queue
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
class AnimeDownloaderService : Service() {
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
private lateinit var builder: NotificationCompat.Builder
|
||||||
|
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
|
||||||
|
|
||||||
|
private val downloadJobs = mutableMapOf<String, Job>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
private var isCurrentlyProcessing = false
|
||||||
|
|
||||||
|
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, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||||
|
setContentTitle("Anime Download Progress")
|
||||||
|
setSmallIcon(R.drawable.ic_round_download_24)
|
||||||
|
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
builder.build(),
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
cancelReceiver,
|
||||||
|
IntentFilter(ACTION_CANCEL_DOWNLOAD),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.clear()
|
||||||
|
downloadJobs.clear()
|
||||||
|
AnimeServiceDataSingleton.isServiceRunning = false
|
||||||
|
unregisterReceiver(cancelReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
snackString("Download started")
|
||||||
|
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
serviceScope.launch {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!isCurrentlyProcessing) {
|
||||||
|
isCurrentlyProcessing = true
|
||||||
|
processQueue()
|
||||||
|
isCurrentlyProcessing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processQueue() {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) {
|
||||||
|
val task = AnimeServiceDataSingleton.downloadQueue.poll()
|
||||||
|
if (task != null) {
|
||||||
|
val job = launch { download(task) }
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs[task.getTaskName()] = job
|
||||||
|
}
|
||||||
|
job.join() // Wait for the job to complete before continuing to the next task
|
||||||
|
mutex.withLock {
|
||||||
|
downloadJobs.remove(task.getTaskName())
|
||||||
|
}
|
||||||
|
updateNotification() // Update the notification after each task is completed
|
||||||
|
}
|
||||||
|
if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopSelf() // Stop the service when the queue is empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun cancelDownload(taskName: String) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
mutex.withLock {
|
||||||
|
val url = AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||||
|
DownloadService.sendRemoveDownload(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
MyDownloadService::class.java,
|
||||||
|
url,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
downloadJobs[taskName]?.cancel()
|
||||||
|
downloadJobs.remove(taskName)
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName }
|
||||||
|
updateNotification() // Update the notification after cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification() {
|
||||||
|
// Update the notification to reflect the current state of the queue
|
||||||
|
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
|
||||||
|
val text = if (pendingDownloads > 0) {
|
||||||
|
"Pending downloads: $pendingDownloads"
|
||||||
|
} else {
|
||||||
|
"All downloads completed"
|
||||||
|
}
|
||||||
|
builder.setContentText(text)
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class) suspend fun download(task: DownloadTask) {
|
||||||
|
try {
|
||||||
|
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
this@AnimeDownloaderService,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastDownloadStarted(task.getTaskName())
|
||||||
|
|
||||||
|
currActivity()?.let {
|
||||||
|
Helper.downloadVideo(
|
||||||
|
it,
|
||||||
|
task.video,
|
||||||
|
task.subtitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
saveMediaInfo(task)
|
||||||
|
downloadsManager.addDownload(
|
||||||
|
Download(
|
||||||
|
task.title,
|
||||||
|
task.episode,
|
||||||
|
Download.Type.ANIME,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// periodically check if the download is complete
|
||||||
|
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||||
|
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||||
|
if (download != null) {
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||||
|
logger("Download failed")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download failed")
|
||||||
|
broadcastDownloadFailed(task.getTaskName())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||||
|
logger("Download completed")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download completed")
|
||||||
|
getSharedPreferences(getString(R.string.anime_downloads), Context.MODE_PRIVATE).edit().putString(
|
||||||
|
task.getTaskName(),
|
||||||
|
task.video.file.url
|
||||||
|
).apply()
|
||||||
|
broadcastDownloadFinished(task.getTaskName())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||||
|
logger("Download stopped")
|
||||||
|
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
snackString("${task.title} - ${task.episode} Download stopped")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
broadcastDownloadProgress(task.getTaskName(), download.percentDownloaded.toInt())
|
||||||
|
builder.setProgress(100, download.percentDownloaded.toInt(), false)
|
||||||
|
if (notifi) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinx.coroutines.delay(2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception while downloading file: ${e.message}")
|
||||||
|
snackString("Exception while downloading file: ${e.message}")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(e)
|
||||||
|
broadcastDownloadFailed(task.getTaskName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
private fun saveMediaInfo(task: DownloadTask) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val directory = File(
|
||||||
|
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Anime/${task.title}"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||||
|
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||||
|
})
|
||||||
|
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||||
|
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
val mediaJson = gson.toJson(task.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private 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@AnimeDownloaderService,
|
||||||
|
"Exception while saving ${name}: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
connection?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadStarted(chapterNumber: String) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFinished(chapterNumber: String) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadFailed(chapterNumber: String) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) {
|
||||||
|
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply {
|
||||||
|
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, chapterNumber)
|
||||||
|
putExtra("progress", progress)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
|
@androidx.annotation.OptIn(UnstableApi::class) override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
|
||||||
|
val taskName = intent.getStringExtra(EXTRA_TASK_NAME)
|
||||||
|
taskName?.let {
|
||||||
|
cancelDownload(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class DownloadTask(
|
||||||
|
val title: String,
|
||||||
|
val episode: String,
|
||||||
|
val video: Video,
|
||||||
|
val subtitle: Subtitle? = null,
|
||||||
|
val sourceMedia: Media? = null,
|
||||||
|
val retries: Int = 2,
|
||||||
|
val simultaneousDownloads: Int = 2,
|
||||||
|
) {
|
||||||
|
fun getTaskName(): String {
|
||||||
|
return "$title - $episode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_ID = 1103
|
||||||
|
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
|
||||||
|
const val EXTRA_TASK_NAME = "extra_task_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AnimeServiceDataSingleton {
|
||||||
|
var video: Video? = null
|
||||||
|
var sourceMedia: Media? = null
|
||||||
|
var downloadQueue: Queue<AnimeDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var isServiceRunning: Boolean = false
|
||||||
|
}
|
|
@ -1,8 +1,17 @@
|
||||||
package ani.dantotsu.download.video
|
package ani.dantotsu.download.video
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
@ -15,6 +24,7 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||||
import androidx.media3.datasource.cache.SimpleCache
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
|
import androidx.media3.exoplayer.offline.Download
|
||||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||||
import androidx.media3.exoplayer.offline.DownloadManager
|
import androidx.media3.exoplayer.offline.DownloadManager
|
||||||
import androidx.media3.exoplayer.offline.DownloadService
|
import androidx.media3.exoplayer.offline.DownloadService
|
||||||
|
@ -22,7 +32,10 @@ import androidx.media3.exoplayer.scheduler.Requirements
|
||||||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
import androidx.media3.ui.TrackSelectionDialogBuilder
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.defaultHeaders
|
import ani.dantotsu.defaultHeaders
|
||||||
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
|
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.okHttpClient
|
import ani.dantotsu.okHttpClient
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
@ -37,6 +50,7 @@ import java.util.concurrent.*
|
||||||
|
|
||||||
object Helper {
|
object Helper {
|
||||||
|
|
||||||
|
var simpleCache: SimpleCache? = null
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||||
val dataSourceFactory = DataSource.Factory {
|
val dataSourceFactory = DataSource.Factory {
|
||||||
|
@ -114,13 +128,13 @@ object Helper {
|
||||||
|
|
||||||
|
|
||||||
private var download: DownloadManager? = null
|
private var download: DownloadManager? = null
|
||||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads"
|
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun downloadManager(context: Context): DownloadManager {
|
fun downloadManager(context: Context): DownloadManager {
|
||||||
return download ?: let {
|
return download ?: let {
|
||||||
val database = StandaloneDatabaseProvider(context)
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
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 dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||||
|
@ -133,17 +147,42 @@ object Helper {
|
||||||
}
|
}
|
||||||
dataSource
|
dataSource
|
||||||
}
|
}
|
||||||
DownloadManager(
|
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||||
|
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||||
|
val downloadManager = DownloadManager(
|
||||||
context,
|
context,
|
||||||
database,
|
database,
|
||||||
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database),
|
getSimpleCache(context),
|
||||||
dataSourceFactory,
|
dataSourceFactory,
|
||||||
Executor(Runnable::run)
|
executorService
|
||||||
).apply {
|
).apply {
|
||||||
requirements =
|
requirements =
|
||||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||||
maxParallelDownloads = 3
|
maxParallelDownloads = 3
|
||||||
}
|
}
|
||||||
|
downloadManager.addListener(
|
||||||
|
object : DownloadManager.Listener { // Override methods of interest here.
|
||||||
|
override fun onDownloadChanged(
|
||||||
|
downloadManager: DownloadManager,
|
||||||
|
download: Download,
|
||||||
|
finalException: Exception?
|
||||||
|
) {
|
||||||
|
if (download.state == Download.STATE_COMPLETED) {
|
||||||
|
Log.e("Downloader", "Download Completed")
|
||||||
|
} else if (download.state == Download.STATE_FAILED) {
|
||||||
|
Log.e("Downloader", "Download Failed")
|
||||||
|
} else if (download.state == Download.STATE_STOPPED) {
|
||||||
|
Log.e("Downloader", "Download Stopped")
|
||||||
|
} else if (download.state == Download.STATE_QUEUED) {
|
||||||
|
Log.e("Downloader", "Download Queued")
|
||||||
|
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||||
|
Log.e("Downloader", "Download Downloading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,4 +198,59 @@ object Helper {
|
||||||
}
|
}
|
||||||
return downloadDirectory!!
|
return downloadDirectory!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startAnimeDownloadService(
|
||||||
|
context: Context,
|
||||||
|
title: String,
|
||||||
|
episode: String,
|
||||||
|
video: Video,
|
||||||
|
subtitle: Subtitle? = null,
|
||||||
|
sourceMedia: Media? = null
|
||||||
|
) {
|
||||||
|
if (!isNotificationPermissionGranted(context)) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
context as Activity,
|
||||||
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadTask = AnimeDownloaderService.DownloadTask(
|
||||||
|
title,
|
||||||
|
episode,
|
||||||
|
video,
|
||||||
|
subtitle,
|
||||||
|
sourceMedia
|
||||||
|
)
|
||||||
|
AnimeServiceDataSingleton.downloadQueue.offer(downloadTask)
|
||||||
|
|
||||||
|
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||||
|
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
AnimeServiceDataSingleton.isServiceRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class) private fun getSimpleCache(context: Context): SimpleCache {
|
||||||
|
return if (simpleCache == null) {
|
||||||
|
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||||
|
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||||
|
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||||
|
simpleCache!!
|
||||||
|
} else {
|
||||||
|
simpleCache!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
return ActivityCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) {
|
class MyDownloadService : DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val JOB_ID = 1
|
private const val JOB_ID = 1
|
||||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||||
|
|
|
@ -424,4 +424,12 @@ class AnimeWatchFragment : Fragment() {
|
||||||
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 ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
|
||||||
|
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
|
||||||
|
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ data class Episode(
|
||||||
var selectedExtractor: String? = null,
|
var selectedExtractor: String? = null,
|
||||||
var selectedVideo: Int = 0,
|
var selectedVideo: Int = 0,
|
||||||
var selectedSubtitle: Int? = -1,
|
var selectedSubtitle: Int? = -1,
|
||||||
var extractors: MutableList<VideoExtractor>? = null,
|
@Transient var extractors: MutableList<VideoExtractor>? = null,
|
||||||
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
|
||||||
var allStreams: Boolean = false,
|
var allStreams: Boolean = false,
|
||||||
var watched: Long? = null,
|
var watched: Long? = null,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ani.dantotsu.*
|
||||||
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
import ani.dantotsu.databinding.BottomSheetSelectorBinding
|
||||||
import ani.dantotsu.databinding.ItemStreamBinding
|
import ani.dantotsu.databinding.ItemStreamBinding
|
||||||
import ani.dantotsu.databinding.ItemUrlBinding
|
import ani.dantotsu.databinding.ItemUrlBinding
|
||||||
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.others.Download.download
|
import ani.dantotsu.others.Download.download
|
||||||
|
@ -214,7 +215,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||||
val extractor = links[position]
|
val extractor = links[position]
|
||||||
holder.binding.streamName.text = extractor.server.name
|
holder.binding.streamName.text = ""//extractor.server.name
|
||||||
|
holder.binding.streamName.visibility = View.GONE
|
||||||
|
|
||||||
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
|
||||||
|
@ -256,10 +258,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
|
||||||
val binding = holder.binding
|
val binding = holder.binding
|
||||||
val video = extractor.videos[position]
|
val video = extractor.videos[position]
|
||||||
binding.urlQuality.text =
|
//binding.urlQuality.text =
|
||||||
if (video.quality != null) "${video.quality}p" else "Default Quality"
|
// if (video.quality != null) "${video.quality}p" else "Default Quality"
|
||||||
binding.urlNote.text = video.extraNote ?: ""
|
//binding.urlNote.text = video.extraNote ?: ""
|
||||||
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
//binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
|
||||||
binding.urlDownload.visibility = View.VISIBLE
|
binding.urlDownload.visibility = View.VISIBLE
|
||||||
binding.urlDownload.setSafeOnClickListener {
|
binding.urlDownload.setSafeOnClickListener {
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
|
||||||
|
@ -267,11 +269,23 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
|
||||||
position
|
position
|
||||||
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
download(
|
//download(
|
||||||
requireActivity(),
|
// requireActivity(),
|
||||||
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
// media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
|
||||||
media!!.userPreferredName
|
// media!!.userPreferredName
|
||||||
)
|
//)
|
||||||
|
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
|
||||||
|
val video = if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||||
|
if (video != null) {
|
||||||
|
Helper.startAnimeDownloadService(
|
||||||
|
requireActivity(),
|
||||||
|
media!!.userPreferredName,
|
||||||
|
episode.number,
|
||||||
|
video,
|
||||||
|
null,
|
||||||
|
media
|
||||||
|
)
|
||||||
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
if (video.format == VideoType.CONTAINER) {
|
if (video.format == VideoType.CONTAINER) {
|
||||||
|
@ -282,11 +296,13 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
"#.##"
|
"#.##"
|
||||||
).format(video.size ?: 0).toString() + " MB"))
|
).format(video.size ?: 0).toString() + " MB"))
|
||||||
} else {
|
} else {
|
||||||
binding.urlQuality.text = "Multi Quality"
|
|
||||||
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
|
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
|
||||||
binding.urlDownload.visibility = View.GONE
|
////binding.urlDownload.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.urlNote.visibility = View.VISIBLE
|
||||||
|
binding.urlNote.text = video.format.name
|
||||||
|
binding.urlQuality.text = extractor.server.name
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = extractor.videos.size
|
override fun getItemCount(): Int = extractor.videos.size
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
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.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -41,6 +42,7 @@ import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Request
|
||||||
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
|
import java.io.File
|
||||||
|
@ -112,7 +114,8 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
seasonGroups.keys.sorted().flatMap { season ->
|
seasonGroups.keys.sorted().flatMap { season ->
|
||||||
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
||||||
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
||||||
val potentialNumber = AnimeNameAdapter.findEpisodeNumber(episode.name)
|
val potentialNumber =
|
||||||
|
AnimeNameAdapter.findEpisodeNumber(episode.name)
|
||||||
if (potentialNumber != null) {
|
if (potentialNumber != null) {
|
||||||
episode.episode_number = potentialNumber
|
episode.episode_number = potentialNumber
|
||||||
} else {
|
} else {
|
||||||
|
@ -613,6 +616,10 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
|
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
|
||||||
|
|
||||||
format = getVideoType(fileName)
|
format = getVideoType(fileName)
|
||||||
|
if (format == null) {
|
||||||
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
format = headRequest(videoUrl, networkHelper)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the format is still undetermined, log an error or handle it appropriately
|
// If the format is still undetermined, log an error or handle it appropriately
|
||||||
|
@ -630,12 +637,12 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
number,
|
number,
|
||||||
format,
|
format,
|
||||||
FileUrl(videoUrl, headersMap),
|
FileUrl(videoUrl, headersMap),
|
||||||
aniVideo.totalContentLength.toDouble()
|
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVideoType(fileName: String): VideoType? {
|
private fun getVideoType(fileName: String): VideoType? {
|
||||||
return when {
|
val type = when {
|
||||||
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
|
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
|
||||||
".mkv",
|
".mkv",
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
|
@ -645,6 +652,47 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? {
|
||||||
|
return try {
|
||||||
|
logger("attempting head request for $fileName")
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(fileName)
|
||||||
|
.head()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
networkHelper.client.newCall(request).execute().use { response ->
|
||||||
|
val contentType = response.header("Content-Type")
|
||||||
|
val contentDisposition = response.header("Content-Disposition")
|
||||||
|
|
||||||
|
if (contentType != null) {
|
||||||
|
when {
|
||||||
|
contentType.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
|
||||||
|
contentType.contains("dash", ignoreCase = true) -> VideoType.DASH
|
||||||
|
contentType.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} else if (contentDisposition != null) {
|
||||||
|
when {
|
||||||
|
contentDisposition.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
|
||||||
|
contentDisposition.contains("dash", ignoreCase = true) -> VideoType.DASH
|
||||||
|
contentDisposition.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger("failed head request for $fileName")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger("Exception in headRequest: $e")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TrackToSubtitle(track: Track): Subtitle {
|
private fun TrackToSubtitle(track: Track): Subtitle {
|
||||||
|
|
|
@ -645,5 +645,6 @@
|
||||||
<string name="add_widget">Add widget</string>
|
<string name="add_widget">Add widget</string>
|
||||||
<string name="app_widget_description">This is an app widget description</string>
|
<string name="app_widget_description">This is an app widget description</string>
|
||||||
<string name="airing_image">Airing Image</string>
|
<string name="airing_image">Airing Image</string>
|
||||||
|
<string name="anime_downloads">animeDownloads</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue