first working version of anime downloads

This commit is contained in:
Finnley Somdahl 2023-12-30 05:12:46 -06:00
parent 41830dba4d
commit d16fbd9a43
19 changed files with 402 additions and 156 deletions

View file

@ -273,7 +273,7 @@
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="true" />
<service
android:name=".download.video.MyDownloadService"
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>

View file

@ -15,43 +15,43 @@ class DownloadsManager(private val context: Context) {
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 }
val novelDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.NOVEL }
val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply()
}
private fun loadDownloads(): List<Download> {
private fun loadDownloads(): List<DownloadedType> {
val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) {
val type = object : TypeToken<List<Download>>() {}.type
val type = object : TypeToken<List<DownloadedType>>() {}.type
gson.fromJson(jsonString, type)
} else {
emptyList()
}
}
fun addDownload(download: Download) {
downloadsList.add(download)
fun addDownload(downloadedType: DownloadedType) {
downloadsList.add(downloadedType)
saveDownloads()
}
fun removeDownload(download: Download) {
downloadsList.remove(download)
removeDirectory(download)
fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(downloadedType)
removeDirectory(downloadedType)
saveDownloads()
}
fun removeMedia(title: String, type: Download.Type) {
val subDirectory = if (type == Download.Type.MANGA) {
fun removeMedia(title: String, type: DownloadedType.Type) {
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == Download.Type.ANIME) {
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
@ -76,16 +76,16 @@ class DownloadsManager(private val context: Context) {
}
private fun cleanDownloads() {
cleanDownload(Download.Type.MANGA)
cleanDownload(Download.Type.ANIME)
cleanDownload(Download.Type.NOVEL)
cleanDownload(DownloadedType.Type.MANGA)
cleanDownload(DownloadedType.Type.ANIME)
cleanDownload(DownloadedType.Type.NOVEL)
}
private fun cleanDownload(type: Download.Type) {
private fun cleanDownload(type: DownloadedType.Type) {
// remove all folders that are not in the downloads list
val subDirectory = if (type == Download.Type.MANGA) {
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == Download.Type.ANIME) {
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
@ -94,18 +94,18 @@ class DownloadsManager(private val context: Context) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory"
)
val downloadsSubList = if (type == Download.Type.MANGA) {
mangaDownloads
} else if (type == Download.Type.ANIME) {
animeDownloads
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
mangaDownloadedTypes
} else if (type == DownloadedType.Type.ANIME) {
animeDownloadedTypes
} else {
novelDownloads
novelDownloadedTypes
}
if (directory.exists()) {
val files = directory.listFiles()
if (files != null) {
for (file in files) {
if (!downloadsSubList.any { it.title == file.name }) {
if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively()
}
}
@ -122,7 +122,7 @@ class DownloadsManager(private val context: Context) {
}
}
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<Download>) //for debugging
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
{
val jsonString = gson.toJson(downloadsList)
val file = File(
@ -138,25 +138,33 @@ class DownloadsManager(private val context: Context) {
file.writeText(jsonString)
}
fun queryDownload(download: Download): Boolean {
return downloadsList.contains(download)
fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(downloadedType)
}
private fun removeDirectory(download: Download) {
val directory = if (download.type == Download.Type.MANGA) {
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
return if (type == null) {
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
}
}
private fun removeDirectory(downloadedType: DownloadedType) {
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}/${download.chapter}"
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}"
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}"
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
@ -173,26 +181,26 @@ class DownloadsManager(private val context: Context) {
}
}
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA) {
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}/${download.chapter}"
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}"
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}"
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
val destination = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${download.title}/${download.chapter}"
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
)
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
@ -206,10 +214,10 @@ class DownloadsManager(private val context: Context) {
}
}
fun purgeDownloads(type: Download.Type) {
val directory = if (type == Download.Type.MANGA) {
fun purgeDownloads(type: DownloadedType.Type) {
val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else if (type == Download.Type.ANIME) {
} else if (type == DownloadedType.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
@ -237,7 +245,7 @@ class DownloadsManager(private val context: Context) {
}
data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME,

View file

@ -8,7 +8,6 @@ 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
@ -18,15 +17,15 @@ 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.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
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.download.video.ExoplayerDownloadService
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.AnimeWatchFragment
@ -44,14 +43,11 @@ 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
@ -161,7 +157,7 @@ class AnimeDownloaderService : Service() {
val url = AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
DownloadService.sendRemoveDownload(
this@AnimeDownloaderService,
MyDownloadService::class.java,
ExoplayerDownloadService::class.java,
url,
false
)
@ -220,16 +216,28 @@ class AnimeDownloaderService : Service() {
}
saveMediaInfo(task)
downloadsManager.addDownload(
Download(
task.title,
task.episode,
Download.Type.ANIME,
)
var continueDownload = false
downloadManager.addListener(
object : androidx.media3.exoplayer.offline.DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
continueDownload = true
}
}
)
//set an async timeout of 30 seconds before setting continueDownload to true
launch {
kotlinx.coroutines.delay(30000)
continueDownload = true
}
// periodically check if the download is complete
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null || continueDownload == false) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
@ -251,6 +259,13 @@ class AnimeDownloaderService : Service() {
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
broadcastDownloadFinished(task.getTaskName())
break
}
@ -284,9 +299,11 @@ class AnimeDownloaderService : Service() {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${task.title}"
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!directory.exists()) directory.mkdirs()
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
@ -305,6 +322,9 @@ class AnimeDownloaderService : Service() {
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
if (task.episodeImage != null) {
downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")
}
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
@ -395,6 +415,7 @@ class AnimeDownloaderService : Service() {
val video: Video,
val subtitle: Subtitle? = null,
val sourceMedia: Media? = null,
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
) {

View file

@ -18,7 +18,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
@ -246,10 +246,10 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(
Download(
DownloadedType(
task.title,
task.chapter,
Download.Type.MANGA
DownloadedType.Type.MANGA
)
)
broadcastDownloadFinished(task.chapter)

View file

@ -2,7 +2,6 @@ package ani.dantotsu.download.manga
import android.animation.ObjectAnimator
import android.content.Context
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -23,20 +22,16 @@ import android.widget.GridView
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat.recreate
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
@ -168,8 +163,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloads.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title }
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
@ -184,10 +179,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnItemLongClickListener { parent, view, position, id ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) {
Download.Type.MANGA
val type: DownloadedType.Type = if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
DownloadedType.Type.MANGA
} else {
Download.Type.NOVEL
DownloadedType.Type.NOVEL
}
// Alert dialog to confirm deletion
val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
@ -292,19 +287,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val _downloads = downloadManager.novelDownloads.filter { it.title == title }
val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
@ -313,17 +308,17 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
private fun getMedia(download: Download): Media? {
val type = if (download.type == Download.Type.MANGA) {
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}"
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
@ -343,23 +338,23 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel {
val type = if (download.type == Download.Type.MANGA) {
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (download.type == Download.Type.ANIME) {
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}"
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
val mediaModel = getMedia(download)!!
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)

View file

@ -17,7 +17,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
@ -330,10 +330,10 @@ class NovelDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(
Download(
DownloadedType(
task.title,
task.chapter,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)

View file

@ -11,7 +11,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R
@UnstableApi
class MyDownloadService : DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
class ExoplayerDownloadService : DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object {
private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1

View file

@ -3,6 +3,7 @@ package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@ -12,6 +13,7 @@ import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
@ -32,6 +34,8 @@ import androidx.media3.exoplayer.scheduler.Requirements
import androidx.media3.ui.TrackSelectionDialogBuilder
import ani.dantotsu.R
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError
@ -50,7 +54,8 @@ import java.util.concurrent.*
object Helper {
var simpleCache: SimpleCache? = null
private var simpleCache: SimpleCache? = null
@SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory {
@ -96,18 +101,18 @@ object Helper {
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
TrackSelectionDialogBuilder(
context, "Select thingy", helper.getTracks(0).groups
/*TrackSelectionDialogBuilder( TODO: use this for subtitles
context, "Select Source", helper.getTracks(0).groups
) { _, overrides ->
val params = TrackSelectionParameters.Builder(context)
overrides.forEach {
params.addOverride(it.value)
}
helper.addTrackSelection(0, params.build())
MyDownloadService
ExoplayerDownloadService
DownloadService.sendAddDownload(
context,
MyDownloadService::class.java,
ExoplayerDownloadService::class.java,
helper.getDownloadRequest(null),
false
)
@ -117,6 +122,14 @@ object Helper {
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
}
build().show()
}*/
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
ExoplayerDownloadService::class.java,
it,
false
)
}
}
@ -160,8 +173,8 @@ object Helper {
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3
}
downloadManager.addListener(
object : DownloadManager.Listener { // Override methods of interest here.
downloadManager.addListener( //for testing
object : DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
@ -199,6 +212,7 @@ object Helper {
return downloadDirectory!!
}
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
title: String,
@ -224,16 +238,62 @@ object Helper {
subtitle,
sourceMedia
)
AnimeServiceDataSingleton.downloadQueue.offer(downloadTask)
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, DownloadedType.Type.ANIME)
if (downloadCheck) {
AlertDialog.Builder(context)
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
DownloadService.sendRemoveDownload(
context,
ExoplayerDownloadService::class.java,
context.getSharedPreferences(
getString(context, R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
downloadTask.getTaskName(),
""
) ?: "",
false
)
context.getSharedPreferences(
getString(context, R.string.anime_downloads),
Context.MODE_PRIVATE
).edit()
.remove(downloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
DownloadedType.Type.ANIME
)
)
AnimeServiceDataSingleton.downloadQueue.offer(downloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
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 {
@OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) {
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val database = Injekt.get<StandaloneDatabaseProvider>()

View file

@ -4,6 +4,7 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.app.DownloadManager
import android.app.PictureInPictureParams
import android.app.PictureInPictureUiState
import android.content.ActivityNotFoundException
@ -97,7 +98,9 @@ import kotlin.math.min
import kotlin.math.roundToInt
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.cast.CastPlayer
import androidx.media3.exoplayer.offline.Download
import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.download.video.Helper
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
@ -150,6 +153,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private var orientationListener: OrientationEventListener? = null
private var downloadId: String? = null
companion object {
var initialized = false
lateinit var media: Media
@ -1115,7 +1120,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (settings.cursedSpeeds)
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
else
arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.15f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f)
arrayOf(
0.25f,
0.33f,
0.5f,
0.66f,
0.75f,
1f,
1.15f,
1.25f,
1.33f,
1.5f,
1.66f,
1.75f,
2f
)
val speedsName = speeds.map { "${it}x" }.toTypedArray()
var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed
@ -1292,7 +1311,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (video?.format == VideoType.CONTAINER || (loadData<Int>("settings_download_manager")
?: 0) != 0
) {
but.visibility = View.VISIBLE
//but.visibility = View.VISIBLE TODO: not sure if this is needed
but.setOnClickListener {
download(this, episode, animeTitle.text.toString())
}
@ -1317,8 +1336,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
dataSource
}
cacheFactory = CacheDataSource.Factory().apply {
setCache(simpleCache)
setCache(Helper.getSimpleCache(this@ExoplayerView))
setUpstreamDataSourceFactory(dataSourceFactory)
setCacheWriteDataSinkFactory(null)
}
val mimeType = when (video?.format) {
@ -1327,6 +1347,20 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
else -> MimeTypes.APPLICATION_MP4
}
val downloadedMediaItem = if (ext.server.offline) {
val key = ext.server.name
downloadId = getSharedPreferences(getString(R.string.anime_downloads), MODE_PRIVATE)
.getString(key, null)
if (downloadId != null) {
Helper.downloadManager(this)
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
} else {
snackString("Download not found")
null
}
} else null
mediaItem = if (downloadedMediaItem == null) {
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
logger("url: ${video!!.file.url}")
logger("mimeType: $mimeType")
@ -1335,7 +1369,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
builder.setSubtitleConfigurations(listofnotnullsubs)
}
mediaItem = builder.build()
builder.build()
} else {
downloadedMediaItem
}
//Source
exoSource.setOnClickListener {
@ -1594,7 +1632,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (isInitialized) {
if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > settings.watchPercentage) {
preloading = true
nextEpisode(false) { i ->
nextEpisode(false) { i -> //TODO: make sure this works for offline episodes
val ep = episodes[episodeArr[currentEpisodeIndex + i]] ?: return@nextEpisode
val selected = media.selected ?: return@nextEpisode
lifecycleScope.launch(Dispatchers.IO) {

View file

@ -29,7 +29,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
@ -166,7 +166,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
chapterAdapter =
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads) {
for (download in downloadManager.mangaDownloadedTypes) {
chapterAdapter.stopDownload(download.chapter)
}
@ -482,10 +482,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
fun onMangaChapterRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
Download(
DownloadedType(
media.nameMAL ?: media.nameRomaji,
i,
Download.Type.MANGA
DownloadedType.Type.MANGA
)
)
chapterAdapter.deleteDownload(i)
@ -500,10 +500,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
Download(
DownloadedType(
media.nameMAL ?: media.nameRomaji,
i,
Download.Type.MANGA
DownloadedType.Type.MANGA
)
)
chapterAdapter.purgeDownload(i)

View file

@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
@ -92,10 +92,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
if (downloadsManager.queryDownload(
Download(
DownloadedType(
media.nameMAL ?: media.nameRomaji,
novel.name,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
) {
@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
return downloadsManager.queryDownload(
Download(
DownloadedType(
media.nameMAL ?: media.nameRomaji,
novel.name,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
}
@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(),
override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.removeDownload(
Download(
DownloadedType(
media.nameMAL ?: media.nameRomaji,
novel.name,
Download.Type.NOVEL
DownloadedType.Type.NOVEL
)
)
}

View file

@ -12,11 +12,17 @@ object AnimeSources : WatchSources() {
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions)
list = createParsersFromExtensions(initialExtensions) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions)
list = createParsersFromExtensions(extensions) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
}
}

View file

@ -616,17 +616,18 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName)
if (format == null) {
val networkHelper = Injekt.get<NetworkHelper>()
format = headRequest(videoUrl, networkHelper)
}
// this solves a problem no one has, so I'm commenting it out for now
//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
if (format == null) {
logger("Unknown video format: $videoUrl")
FirebaseCrashlytics.getInstance()
.recordException(Exception("Unknown video format: $videoUrl"))
//FirebaseCrashlytics.getInstance()
// .recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER
}
val headersMap: Map<String, String> =

View file

@ -46,6 +46,19 @@ abstract class WatchSources : BaseSources() {
sEpisode = it.sEpisode
)
}
} else if (parser is OfflineAnimeParser) {
parser.loadEpisodes(showLink, extra, SAnime.create()).forEach {
map[it.number] = Episode(
it.number,
it.link,
it.title,
it.description,
it.thumbnail,
it.isFiller,
extra = it.extra,
sEpisode = it.sEpisode
)
}
}
}
return map

View file

@ -7,9 +7,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {

View file

@ -0,0 +1,106 @@
package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.anime.AnimeNameAdapter
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineAnimeParser : AnimeParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val name = "Offline"
override val saveName = "Offline"
override val hostUrl = "Offline"
override val isDubAvailableSeparately = false
override val isNSFW = false
override suspend fun loadEpisodes(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/$animeLink"
)
//get all of the folder names and add them to the list
val episodes = mutableListOf<Episode>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val episode = Episode(
it.name,
"$animeLink - ${it.name}",
it.name,
null,
null,
sEpisode = SEpisodeImpl()
)
episodes.add(episode)
}
}
episodes.sortBy { AnimeNameAdapter.findEpisodeNumber(it.number) }
return episodes
}
return emptyList()
}
override suspend fun loadVideoServers(
episodeLink: String,
extra: Map<String, String>?,
sEpisode: SEpisode
): List<VideoServer> {
return listOf(
VideoServer(
episodeLink,
offline = true
)
)
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
returnTitles.add(title)
}
}
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
returnList.add(ShowResponse(title, title, title))
}
return returnList
}
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
return OfflineVideoExtractor(server)
}
}
class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer
get() = videoServer
override suspend fun extract(): VideoContainer {
val sublist = emptyList<Subtitle>()
//we need to return a "fake" video so that the app doesn't crash
val video = Video(
null,
VideoType.CONTAINER,
"",
)
return VideoContainer(listOf(video), sublist)
}
}

View file

@ -76,7 +76,7 @@ class OfflineMangaParser : MangaParser() {
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {

View file

@ -3,10 +3,7 @@ package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaNameAdapter
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() {
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.novelDownloads.map { it.title }.distinct()
val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {

View file

@ -57,11 +57,15 @@ data class VideoServer(
val name: String,
val embed: FileUrl,
val extraData: Map<String, String>? = null,
val video: eu.kanade.tachiyomi.animesource.model.Video? = null
val video: eu.kanade.tachiyomi.animesource.model.Video? = null,
val offline: Boolean = false
) : Serializable {
constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null)
: this(name, FileUrl(embedUrl), extraData)
constructor(name: String, offline: Boolean)
: this(name, FileUrl(""), null, null, offline)
constructor(
name: String,
embedUrl: String,