[feat] Complete video caching system
This commit is contained in:
@@ -105,9 +105,8 @@ class AbyssTunnelProxy @Inject constructor(
|
||||
val read = localIn.read(buffer)
|
||||
if (read <= 0)
|
||||
break
|
||||
Log.i("Delay Analyze", "Read $read Bytes from HttpClient")
|
||||
|
||||
abyss.write(buffer, 0, read)
|
||||
Log.i("Delay Analyze", "Wrote $read Bytes to Remote Abyss")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +117,7 @@ class AbyssTunnelProxy @Inject constructor(
|
||||
val n = abyss.read(buffer, 0, buffer.size)
|
||||
if (n <= 0)
|
||||
break
|
||||
Log.i("Delay Analyze", "Read $n Bytes from Remote Abyss")
|
||||
localOut.write(buffer, 0, n)
|
||||
Log.i("Delay Analyze", "Wrote $n Bytes to HttpClient")
|
||||
}
|
||||
}
|
||||
}
|
@@ -217,13 +217,10 @@ object ApiClient {
|
||||
domain = selectedUrl.toHttpUrlOrNull()?.host ?: ""
|
||||
cert = crt
|
||||
base = selectedUrl
|
||||
|
||||
withContext(Dispatchers.IO)
|
||||
{
|
||||
(context as AetherApp).abyssService?.proxy?.config(base.toUri().host!!, 4096)
|
||||
context.abyssService?.downloader?.init()
|
||||
(context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096)
|
||||
}
|
||||
|
||||
api = createRetrofit().create(ApiInterface::class.java)
|
||||
|
||||
Log.i("Delay Analyze", "Start Abyss Hello")
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.acitelight.aether.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.acitelight.aether.Screen
|
||||
import com.acitelight.aether.model.Video
|
||||
import com.acitelight.aether.service.ApiClient.createOkHttp
|
||||
@@ -13,7 +14,18 @@ import com.tonyodev.fetch2.Status
|
||||
import com.tonyodev.fetch2core.Extras
|
||||
import com.tonyodev.fetch2okhttp.OkHttpDownloader
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -23,21 +35,25 @@ class FetchManager @Inject constructor(
|
||||
) {
|
||||
private var fetch: Fetch? = null
|
||||
private var listener: FetchListener? = null
|
||||
private var client: OkHttpClient? = null
|
||||
val configured = MutableStateFlow(false)
|
||||
|
||||
fun init()
|
||||
{
|
||||
client = createOkHttp()
|
||||
val fetchConfiguration = FetchConfiguration.Builder(context)
|
||||
.setDownloadConcurrentLimit(8)
|
||||
.setHttpDownloader(OkHttpDownloader(createOkHttp()))
|
||||
.setHttpDownloader(OkHttpDownloader(client))
|
||||
.build()
|
||||
|
||||
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
||||
fetch = Fetch.Impl.getInstance(fetchConfiguration)
|
||||
configured.update { true }
|
||||
}
|
||||
|
||||
// listener management
|
||||
fun setListener(l: FetchListener) {
|
||||
if (fetch == null)
|
||||
return
|
||||
suspend fun setListener(l: FetchListener) {
|
||||
configured.filter { it }.first()
|
||||
|
||||
listener?.let { fetch?.removeListener(it) }
|
||||
listener = l
|
||||
fetch?.addListener(l)
|
||||
@@ -51,14 +67,24 @@ class FetchManager @Inject constructor(
|
||||
}
|
||||
|
||||
// query downloads
|
||||
fun getAllDownloads(callback: (List<Download>) -> Unit) {
|
||||
if (fetch == null) init()
|
||||
suspend fun getAllDownloads(callback: (List<Download>) -> Unit) {
|
||||
configured.filter { it }.first()
|
||||
fetch?.getDownloads { list -> callback(list) } ?: callback(emptyList())
|
||||
}
|
||||
|
||||
fun getDownloadsByStatus(status: Status, callback: (List<Download>) -> Unit) {
|
||||
if (fetch == null) init()
|
||||
fetch?.getDownloadsWithStatus(status) { list -> callback(list) } ?: callback(emptyList())
|
||||
suspend fun getAllDownloadsAsync(): List<Download>
|
||||
{
|
||||
configured.filter { it }.first()
|
||||
val completed = MutableStateFlow(false)
|
||||
var r = listOf<Download>()
|
||||
|
||||
fetch?.getDownloads { list ->
|
||||
r = list
|
||||
completed.update { true }
|
||||
}
|
||||
|
||||
completed.filter { it }.first()
|
||||
return r
|
||||
}
|
||||
|
||||
// operations
|
||||
@@ -80,13 +106,13 @@ class FetchManager @Inject constructor(
|
||||
} ?: callback?.invoke()
|
||||
}
|
||||
|
||||
private fun enqueue(request: Request, onEnqueued: ((Request) -> Unit)? = null, onError: ((com.tonyodev.fetch2.Error) -> Unit)? = null) {
|
||||
if (fetch == null) init()
|
||||
private suspend fun enqueue(request: Request, onEnqueued: ((Request) -> Unit)? = null, onError: ((com.tonyodev.fetch2.Error) -> Unit)? = null) {
|
||||
configured.filter { it }.first()
|
||||
fetch?.enqueue(request, { r -> onEnqueued?.invoke(r) }, { e -> onError?.invoke(e) })
|
||||
}
|
||||
|
||||
private fun getVideosDirectory() {
|
||||
val appFilesDir = context.filesDir
|
||||
val appFilesDir = context.getExternalFilesDir(null)
|
||||
val videosDir = File(appFilesDir, "videos")
|
||||
|
||||
if (!videosDir.exists()) {
|
||||
@@ -94,12 +120,52 @@ class FetchManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun startVideoDownload(video: Video)
|
||||
suspend fun downloadFile(
|
||||
client: OkHttpClient,
|
||||
url: String,
|
||||
destFile: File
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = okhttp3.Request.Builder().url(url).build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return@withContext Result.failure(IOException("Unexpected code $response"))
|
||||
}
|
||||
|
||||
destFile.parentFile?.mkdirs()
|
||||
response.body.byteStream().use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startVideoDownload(video: Video)
|
||||
{
|
||||
val path = File(context.filesDir, "videos/${video.klass}/${video.id}/video.mp4")
|
||||
val path = File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/video.mp4")
|
||||
val request = Request(video.getVideo(), path.path).apply {
|
||||
extras = Extras(mapOf("name" to video.video.name, "id" to video.id, "class" to video.klass))
|
||||
}
|
||||
|
||||
downloadFile(
|
||||
client!!,
|
||||
video.getCover(),
|
||||
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/cover.jpg"))
|
||||
|
||||
enqueue(request)
|
||||
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/summary.json").writeText(Json.encodeToString(video))
|
||||
|
||||
for(p in video.getGallery())
|
||||
{
|
||||
downloadFile(
|
||||
client!!,
|
||||
p.url,
|
||||
File(context.getExternalFilesDir(null), "videos/${video.klass}/${video.id}/gallery/${p.name}"))
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,14 +5,18 @@ import com.acitelight.aether.model.BookMark
|
||||
import com.acitelight.aether.model.Comic
|
||||
import com.acitelight.aether.model.ComicResponse
|
||||
import com.acitelight.aether.model.Video
|
||||
import com.tonyodev.fetch2.Status
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
class MediaManager @Inject constructor(
|
||||
|
||||
val fetchManager: FetchManager,
|
||||
@ApplicationContext val context: Context
|
||||
)
|
||||
{
|
||||
var token: String = "null"
|
||||
@@ -43,23 +47,86 @@ class MediaManager @Inject constructor(
|
||||
|
||||
suspend fun queryVideo(klass: String, id: String): Video?
|
||||
{
|
||||
val downloaded = fetchManager.getAllDownloadsAsync().filter {
|
||||
it.status == Status.COMPLETED &&
|
||||
it.extras.getString("id", "") == id &&
|
||||
it.extras.getString("class", "") == klass
|
||||
}
|
||||
|
||||
if(!downloaded.isEmpty())
|
||||
{
|
||||
val jsonString = File(
|
||||
context.getExternalFilesDir(null),
|
||||
"videos/$klass/$id/summary.json"
|
||||
).readText()
|
||||
return Json.decodeFromString<Video>(jsonString).toLocal(context.getExternalFilesDir(null)?.path!!)
|
||||
}
|
||||
|
||||
try {
|
||||
val j = ApiClient.api!!.queryVideo(klass, id, token)
|
||||
return Video(klass = klass, id = id, token=token, isLocal = false, video = j)
|
||||
return Video(klass = klass, id = id, token=token, isLocal = false, localBase = "", video = j)
|
||||
}catch (e: Exception)
|
||||
{
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun queryVideoBulk(klass: String, id: List<String>): List<Video>?
|
||||
{
|
||||
try {
|
||||
val j = ApiClient.api!!.queryVideoBulk(klass, id, token)
|
||||
return j.zip(id).map {Video(klass = klass, id = it.second, token=token, isLocal = false, video = it.first)}
|
||||
}catch (e: Exception)
|
||||
{
|
||||
return null
|
||||
suspend fun queryVideoBulk(klass: String, id: List<String>): List<Video>? {
|
||||
return try {
|
||||
val completedDownloads = fetchManager.getAllDownloadsAsync()
|
||||
.filter { it.status == Status.COMPLETED }
|
||||
val localIds = mutableSetOf<String>()
|
||||
val remoteIds = mutableListOf<String>()
|
||||
|
||||
for (videoId in id) {
|
||||
if (completedDownloads.any {
|
||||
it.extras.getString("id", "") == videoId &&
|
||||
it.extras.getString("class", "") == klass
|
||||
}) {
|
||||
localIds.add(videoId)
|
||||
} else {
|
||||
remoteIds.add(videoId)
|
||||
}
|
||||
}
|
||||
|
||||
val localVideos = localIds.mapNotNull { videoId ->
|
||||
val localFile = File(
|
||||
context.getExternalFilesDir(null),
|
||||
"videos/$klass/$videoId/summary.json"
|
||||
)
|
||||
if (localFile.exists()) {
|
||||
try {
|
||||
val jsonString = localFile.readText()
|
||||
Json.decodeFromString<Video>(jsonString).toLocal(
|
||||
context.getExternalFilesDir(null)?.path ?: ""
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val remoteVideos = if (remoteIds.isNotEmpty()) {
|
||||
val j = ApiClient.api!!.queryVideoBulk(klass, remoteIds, token)
|
||||
j.zip(remoteIds).map {
|
||||
Video(
|
||||
klass = klass,
|
||||
id = it.second,
|
||||
token = token,
|
||||
isLocal = false,
|
||||
localBase = "",
|
||||
video = it.first
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
localVideos + remoteVideos
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -91,7 +91,7 @@ class RecentManager @Inject constructor(
|
||||
|
||||
recent.removeAt(index)
|
||||
}
|
||||
recent.add(0, mediaManager.queryVideoBulk(video.klass, listOf(video.id))!![0])
|
||||
recent.add(0, mediaManager.queryVideo(video.klass, video.id)!!)
|
||||
|
||||
|
||||
if(recent.size >= 21)
|
||||
|
@@ -0,0 +1,20 @@
|
||||
package com.acitelight.aether.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import com.acitelight.aether.model.Video
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VideoLibrary @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
var classes = mutableStateListOf<String>()
|
||||
val classesMap = mutableStateMapOf<String, SnapshotStateList<Video>>()
|
||||
val updatingMap: MutableMap<Int, Boolean> = mutableMapOf()
|
||||
}
|
Reference in New Issue
Block a user