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:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="true" /> android:exported="true" />
<service <service
android:name=".download.video.MyDownloadService" android:name=".download.video.ExoplayerDownloadService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync"> android:foregroundServiceType="dataSync">
<intent-filter> <intent-filter>

View file

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

View file

@ -8,7 +8,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.IBinder import android.os.IBinder
@ -18,15 +17,15 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi 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 androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager 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.Helper
import ani.dantotsu.download.video.MyDownloadService import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.AnimeWatchFragment 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.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -161,7 +157,7 @@ class AnimeDownloaderService : Service() {
val url = AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url ?: "" val url = AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
DownloadService.sendRemoveDownload( DownloadService.sendRemoveDownload(
this@AnimeDownloaderService, this@AnimeDownloaderService,
MyDownloadService::class.java, ExoplayerDownloadService::class.java,
url, url,
false false
) )
@ -220,16 +216,28 @@ class AnimeDownloaderService : Service() {
} }
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload( var continueDownload = false
Download( downloadManager.addListener(
task.title, object : androidx.media3.exoplayer.offline.DownloadManager.Listener {
task.episode, override fun onDownloadChanged(
Download.Type.ANIME, 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 // 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) val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) { if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
@ -251,6 +259,13 @@ class AnimeDownloaderService : Service() {
task.getTaskName(), task.getTaskName(),
task.video.file.url task.video.file.url
).apply() ).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
broadcastDownloadFinished(task.getTaskName()) broadcastDownloadFinished(task.getTaskName())
break break
} }
@ -284,9 +299,11 @@ class AnimeDownloaderService : Service() {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${task.title}" "${DownloadsManager.animeLocation}/${task.title}"
) )
val episodeDirectory = File(directory, task.episode)
if (!directory.exists()) directory.mkdirs() if (!directory.exists()) directory.mkdirs()
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json") val file = File(directory, "media.json")
val gson = GsonBuilder() val gson = GsonBuilder()
@ -305,6 +322,9 @@ class AnimeDownloaderService : Service() {
if (media != null) { if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") } media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.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) val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -395,6 +415,7 @@ class AnimeDownloaderService : Service() {
val video: Video, val video: Video,
val subtitle: Subtitle? = null, val subtitle: Subtitle? = null,
val sourceMedia: Media? = null, val sourceMedia: Media? = null,
val episodeImage: String? = null,
val retries: Int = 2, val retries: Int = 2,
val simultaneousDownloads: 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.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -246,10 +246,10 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload( downloadsManager.addDownload(
Download( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
Download.Type.MANGA DownloadedType.Type.MANGA
) )
) )
broadcastDownloadFinished(task.chapter) broadcastDownloadFinished(task.chapter)

View file

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

View file

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

View file

@ -11,7 +11,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R import ani.dantotsu.R
@UnstableApi @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 { 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

View file

@ -3,6 +3,7 @@ package ani.dantotsu.download.video
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -12,6 +13,7 @@ import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
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
@ -32,6 +34,8 @@ 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.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError import ani.dantotsu.logError
@ -50,7 +54,8 @@ import java.util.concurrent.*
object Helper { object Helper {
var simpleCache: SimpleCache? = null private 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 {
@ -96,18 +101,18 @@ object Helper {
) )
downloadHelper.prepare(object : DownloadHelper.Callback { downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) { override fun onPrepared(helper: DownloadHelper) {
TrackSelectionDialogBuilder( /*TrackSelectionDialogBuilder( TODO: use this for subtitles
context, "Select thingy", helper.getTracks(0).groups context, "Select Source", helper.getTracks(0).groups
) { _, overrides -> ) { _, overrides ->
val params = TrackSelectionParameters.Builder(context) val params = TrackSelectionParameters.Builder(context)
overrides.forEach { overrides.forEach {
params.addOverride(it.value) params.addOverride(it.value)
} }
helper.addTrackSelection(0, params.build()) helper.addTrackSelection(0, params.build())
MyDownloadService ExoplayerDownloadService
DownloadService.sendAddDownload( DownloadService.sendAddDownload(
context, context,
MyDownloadService::class.java, ExoplayerDownloadService::class.java,
helper.getDownloadRequest(null), helper.getDownloadRequest(null),
false false
) )
@ -117,6 +122,14 @@ object Helper {
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)" if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
} }
build().show() build().show()
}*/
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
ExoplayerDownloadService::class.java,
it,
false
)
} }
} }
@ -149,7 +162,7 @@ object Helper {
} }
val threadPoolSize = Runtime.getRuntime().availableProcessors() val threadPoolSize = Runtime.getRuntime().availableProcessors()
val executorService = Executors.newFixedThreadPool(threadPoolSize) val executorService = Executors.newFixedThreadPool(threadPoolSize)
val downloadManager = DownloadManager( val downloadManager = DownloadManager(
context, context,
database, database,
getSimpleCache(context), getSimpleCache(context),
@ -160,15 +173,15 @@ object Helper {
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3 maxParallelDownloads = 3
} }
downloadManager.addListener( downloadManager.addListener( //for testing
object : DownloadManager.Listener { // Override methods of interest here. object : DownloadManager.Listener {
override fun onDownloadChanged( override fun onDownloadChanged(
downloadManager: DownloadManager, downloadManager: DownloadManager,
download: Download, download: Download,
finalException: Exception? finalException: Exception?
) { ) {
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
Log.e("Downloader", "Download Completed") Log.e("Downloader", "Download Completed")
} else if (download.state == Download.STATE_FAILED) { } else if (download.state == Download.STATE_FAILED) {
Log.e("Downloader", "Download Failed") Log.e("Downloader", "Download Failed")
} else if (download.state == Download.STATE_STOPPED) { } else if (download.state == Download.STATE_STOPPED) {
@ -199,6 +212,7 @@ object Helper {
return downloadDirectory!! return downloadDirectory!!
} }
@OptIn(UnstableApi::class)
fun startAnimeDownloadService( fun startAnimeDownloadService(
context: Context, context: Context,
title: String, title: String,
@ -224,16 +238,62 @@ object Helper {
subtitle, subtitle,
sourceMedia sourceMedia
) )
AnimeServiceDataSingleton.downloadQueue.offer(downloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) { val downloadsManger = Injekt.get<DownloadsManager>()
val intent = Intent(context, AnimeDownloaderService::class.java) val downloadCheck = downloadsManger
ContextCompat.startForegroundService(context, intent) .queryDownload(title, episode, DownloadedType.Type.ANIME)
AnimeServiceDataSingleton.isServiceRunning = true
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) { return if (simpleCache == null) {
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val database = Injekt.get<StandaloneDatabaseProvider>() val database = Injekt.get<StandaloneDatabaseProvider>()

View file

@ -4,6 +4,7 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.Dialog import android.app.Dialog
import android.app.DownloadManager
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.app.PictureInPictureUiState import android.app.PictureInPictureUiState
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -97,7 +98,9 @@ import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.cast.CastPlayer import androidx.media3.cast.CastPlayer
import androidx.media3.exoplayer.offline.Download
import androidx.mediarouter.app.MediaRouteButton 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.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext 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 orientationListener: OrientationEventListener? = null
private var downloadId: String? = null
companion object { companion object {
var initialized = false var initialized = false
lateinit var media: Media lateinit var media: Media
@ -475,7 +480,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (isInitialized) { if (isInitialized) {
isPlayerPlaying = exoPlayer.isPlaying isPlayerPlaying = exoPlayer.isPlaying
(exoPlay.drawable as Animatable?)?.start() (exoPlay.drawable as Animatable?)?.start()
if (isPlayerPlaying || castPlayer.isPlaying ) { if (isPlayerPlaying || castPlayer.isPlaying) {
Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay) Glide.with(this).load(R.drawable.anim_play_to_pause).into(exoPlay)
exoPlayer.pause() exoPlayer.pause()
castPlayer.pause() castPlayer.pause()
@ -1115,7 +1120,21 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (settings.cursedSpeeds) if (settings.cursedSpeeds)
arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f) arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f)
else 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() val speedsName = speeds.map { "${it}x" }.toTypedArray()
var curSpeed = loadData("${media.id}_speed", this) ?: settings.defaultSpeed 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") if (video?.format == VideoType.CONTAINER || (loadData<Int>("settings_download_manager")
?: 0) != 0 ?: 0) != 0
) { ) {
but.visibility = View.VISIBLE //but.visibility = View.VISIBLE TODO: not sure if this is needed
but.setOnClickListener { but.setOnClickListener {
download(this, episode, animeTitle.text.toString()) download(this, episode, animeTitle.text.toString())
} }
@ -1317,8 +1336,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
dataSource dataSource
} }
cacheFactory = CacheDataSource.Factory().apply { cacheFactory = CacheDataSource.Factory().apply {
setCache(simpleCache) setCache(Helper.getSimpleCache(this@ExoplayerView))
setUpstreamDataSourceFactory(dataSourceFactory) setUpstreamDataSourceFactory(dataSourceFactory)
setCacheWriteDataSinkFactory(null)
} }
val mimeType = when (video?.format) { val mimeType = when (video?.format) {
@ -1327,15 +1347,33 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
else -> MimeTypes.APPLICATION_MP4 else -> MimeTypes.APPLICATION_MP4
} }
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) val downloadedMediaItem = if (ext.server.offline) {
logger("url: ${video!!.file.url}") val key = ext.server.name
logger("mimeType: $mimeType") 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
if (sub != null) { mediaItem = if (downloadedMediaItem == null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull() val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
builder.setSubtitleConfigurations(listofnotnullsubs) logger("url: ${video!!.file.url}")
logger("mimeType: $mimeType")
if (sub != null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull()
builder.setSubtitleConfigurations(listofnotnullsubs)
}
builder.build()
} else {
downloadedMediaItem
} }
mediaItem = builder.build()
//Source //Source
exoSource.setOnClickListener { exoSource.setOnClickListener {
@ -1457,7 +1495,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoPlayer.release() exoPlayer.release()
VideoCache.release() VideoCache.release()
mediaSession?.release() mediaSession?.release()
if(DiscordServiceRunningSingleton.running) { if (DiscordServiceRunningSingleton.running) {
val stopIntent = Intent(this, DiscordService::class.java) val stopIntent = Intent(this, DiscordService::class.java)
DiscordServiceRunningSingleton.running = false DiscordServiceRunningSingleton.running = false
stopService(stopIntent) stopService(stopIntent)
@ -1594,7 +1632,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (isInitialized) { if (isInitialized) {
if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > settings.watchPercentage) { if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > settings.watchPercentage) {
preloading = true 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 ep = episodes[episodeArr[currentEpisodeIndex + i]] ?: return@nextEpisode
val selected = media.selected ?: return@nextEpisode val selected = media.selected ?: return@nextEpisode
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {

View file

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

View file

@ -22,7 +22,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton import ani.dantotsu.download.novel.NovelServiceDataSingleton
@ -92,10 +92,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean { override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
if (downloadsManager.queryDownload( if (downloadsManager.queryDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.nameMAL ?: media.nameRomaji,
novel.name, novel.name,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
) { ) {
@ -124,10 +124,10 @@ class NovelReadFragment : Fragment(),
override fun downloadedCheck(novel: ShowResponse): Boolean { override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
return downloadsManager.queryDownload( return downloadsManager.queryDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.nameMAL ?: media.nameRomaji,
novel.name, novel.name,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
} }
@ -135,10 +135,10 @@ class NovelReadFragment : Fragment(),
override fun deleteDownload(novel: ShowResponse) { override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.removeDownload( downloadsManager.removeDownload(
Download( DownloadedType(
media.nameMAL ?: media.nameRomaji, media.nameMAL ?: media.nameRomaji,
novel.name, 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>>) { suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>) {
// Initialize with the first value from StateFlow // Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first() val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions) list = createParsersFromExtensions(initialExtensions) + Lazier(
{ OfflineAnimeParser() },
"Downloaded"
)
// Update as StateFlow emits new values // Update as StateFlow emits new values
fromExtensions.collect { extensions -> 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 ?: "" val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName) format = getVideoType(fileName)
if (format == null) { // this solves a problem no one has, so I'm commenting it out for now
val networkHelper = Injekt.get<NetworkHelper>() //if (format == null) {
format = headRequest(videoUrl, networkHelper) // 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) { if (format == null) {
logger("Unknown video format: $videoUrl") logger("Unknown video format: $videoUrl")
FirebaseCrashlytics.getInstance() //FirebaseCrashlytics.getInstance()
.recordException(Exception("Unknown video format: $videoUrl")) // .recordException(Exception("Unknown video format: $videoUrl"))
format = VideoType.CONTAINER format = VideoType.CONTAINER
} }
val headersMap: Map<String, String> = val headersMap: Map<String, String> =

View file

@ -46,6 +46,19 @@ abstract class WatchSources : BaseSources() {
sEpisode = it.sEpisode 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 return map

View file

@ -7,9 +7,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() { object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList() override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) { 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> { 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() val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) { for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {

View file

@ -3,10 +3,7 @@ package ani.dantotsu.parsers
import android.os.Environment import android.os.Environment
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaNameAdapter 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 me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -53,7 +50,7 @@ class OfflineNovelParser: NovelParser() {
} }
override suspend fun search(query: String): List<ShowResponse> { 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() val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) { for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {

View file

@ -57,11 +57,15 @@ data class VideoServer(
val name: String, val name: String,
val embed: FileUrl, val embed: FileUrl,
val extraData: Map<String, String>? = null, 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 { ) : Serializable {
constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null) constructor(name: String, embedUrl: String, extraData: Map<String, String>? = null)
: this(name, FileUrl(embedUrl), extraData) : this(name, FileUrl(embedUrl), extraData)
constructor(name: String, offline: Boolean)
: this(name, FileUrl(""), null, null, offline)
constructor( constructor(
name: String, name: String,
embedUrl: String, embedUrl: String,