fix: downloads working on android

This commit is contained in:
Fredrik Burmester
2025-02-18 12:15:09 +01:00
parent cdd8d921fd
commit 0db798c9af
60 changed files with 122 additions and 38 deletions

View File

@@ -101,7 +101,7 @@ export default function Index() {
const downloadsDir = FileSystem.documentDirectory + "downloads/";
await FileSystem.deleteAsync(downloadsDir + id + ".json");
await FileSystem.deleteAsync(downloadsDir + id);
refetchDownloadedFiles()
refetchDownloadedFiles();
};
return (

View File

@@ -11,12 +11,12 @@ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.ab
def kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
@@ -29,6 +29,7 @@ if (useManagedAndroidSdkVersions) {
classpath "com.android.tools.build:gradle:7.1.3"
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
@@ -42,39 +43,42 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
// Media3 dependencies
def media3Version = "1.2.0"
implementation "androidx.media3:media3-exoplayer:$media3Version"
implementation "androidx.media3:media3-datasource:$media3Version"
implementation "androidx.media3:media3-common:$media3Version"
implementation "androidx.media3:media3-database:$media3Version"
implementation "androidx.media3:media3-decoder:$media3Version"
implementation "androidx.media3:media3-ui:$media3Version"
// Coroutines for background processing
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
def media3_version = "1.2.1"
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
implementation "androidx.media3:media3-database:$media3_version"
implementation "androidx.media3:media3-datasource:$media3_version"
}
android {
namespace "expo.modules.hlsdownloader"
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "0.1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
lintOptions {
abortOnError false
}
}
kotlin {
jvmToolchain(17)
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += ["-Xshow-kotlin-compiler-errors"]

View File

@@ -17,6 +17,7 @@ import java.io.File
import java.util.concurrent.Executors
class HlsDownloaderModule : Module() {
private val TAG = "HlsDownloaderModule"
private var activeDownloads = mutableMapOf<String, DownloadMetadata>()
private lateinit var downloadManager: DownloadManager
private lateinit var downloadCache: SimpleCache
@@ -38,6 +39,7 @@ class HlsDownloaderModule : Module() {
)
OnCreate {
android.util.Log.d(TAG, "Creating HLS Downloader module")
val context = appContext.reactContext as Context
val cacheDir = File(context.getExternalFilesDir(null), "downloads")
if (!cacheDir.exists()) {
@@ -65,10 +67,13 @@ class HlsDownloaderModule : Module() {
download: Download,
finalException: Exception?
) {
android.util.Log.d(TAG, "Download changed - State: ${download.state}, Progress: ${download.percentDownloaded}%")
val metadata = activeDownloads[download.request.id]
if (metadata != null) {
when (download.state) {
Download.STATE_COMPLETED -> {
android.util.Log.d(TAG, "Download completed for ${metadata.providedId}")
sendEvent(
"onComplete",
mapOf(
@@ -83,6 +88,7 @@ class HlsDownloaderModule : Module() {
saveMetadataFile(metadata)
}
Download.STATE_FAILED -> {
android.util.Log.e(TAG, "Download failed for ${metadata.providedId}", finalException)
sendEvent(
"onError",
mapOf(
@@ -100,6 +106,8 @@ class HlsDownloaderModule : Module() {
download.bytesDownloaded.toFloat() / download.contentLength
} else 0f
android.util.Log.d(TAG, "Download progress for ${metadata.providedId}: $progress")
sendEvent(
"onProgress",
mapOf(
@@ -108,7 +116,10 @@ class HlsDownloaderModule : Module() {
"state" to when (download.state) {
Download.STATE_DOWNLOADING -> "DOWNLOADING"
Download.STATE_QUEUED -> "PENDING"
else -> "DOWNLOADING"
Download.STATE_STOPPED -> "STOPPED"
Download.STATE_REMOVING -> "REMOVING"
Download.STATE_RESTARTING -> "RESTARTING"
else -> "UNKNOWN"
},
"metadata" to metadata.metadata,
"startTime" to metadata.startTime,
@@ -117,9 +128,24 @@ class HlsDownloaderModule : Module() {
)
}
}
} else {
android.util.Log.w(TAG, "Received download update for unknown download id: ${download.request.id}")
}
}
override fun onDownloadsPausedChanged(
downloadManager: DownloadManager,
downloadsPaused: Boolean
) {
android.util.Log.d(TAG, "Downloads paused changed: $downloadsPaused")
}
override fun onIdle(downloadManager: DownloadManager) {
android.util.Log.d(TAG, "Download manager is idle")
}
})
downloadManager.resumeDownloads()
}
Function("getActiveDownloads") {
@@ -135,6 +161,7 @@ class HlsDownloaderModule : Module() {
}
Function("downloadHLSAsset") { providedId: String, url: String, metadata: Map<String, Any>? ->
android.util.Log.d(TAG, "Starting download for $providedId from $url")
val startTime = System.currentTimeMillis()
val context = appContext.reactContext as Context
@@ -158,10 +185,11 @@ class HlsDownloaderModule : Module() {
providedId,
Uri.parse(url)
)
.setCustomCacheKey(providedId)
.setStreamKeys(emptyList())
.build()
downloadManager.addDownload(downloadRequest)
android.util.Log.d(TAG, "Download request added for $providedId")
activeDownloads[providedId] = DownloadMetadata(
providedId = providedId,
@@ -181,6 +209,7 @@ class HlsDownloaderModule : Module() {
)
} catch (e: Exception) {
android.util.Log.e(TAG, "Error starting download for $providedId", e)
sendEvent(
"onError",
mapOf(
@@ -224,4 +253,5 @@ class HlsDownloaderModule : Module() {
e.printStackTrace()
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,7 @@ import {
import { toast } from "sonner-native";
import { apiAtom, userAtom } from "./JellyfinProvider";
import { useFocusEffect } from "expo-router";
import { AppState, AppStateStatus } from "react-native";
import { AppState, AppStateStatus, Platform } from "react-native";
type DownloadOptionsData = {
selectedAudioStream: number;
@@ -69,34 +69,55 @@ export type DownloadedFileInfo = {
};
const getDownloadedFiles = async (): Promise<DownloadedFileInfo[]> => {
const downloaded: DownloadedFileInfo[] = [];
console.log("getDownloadedFiles ~");
const downloadsDir = FileSystem.documentDirectory + "downloads/";
const dirInfo = await FileSystem.getInfoAsync(downloadsDir);
const files = await FileSystem.readDirectoryAsync(
FileSystem.documentDirectory!
);
console.log(files);
if (!dirInfo.exists) return [];
return [];
const files = await FileSystem.readDirectoryAsync(downloadsDir);
// const downloaded: DownloadedFileInfo[] = [];
for (let file of files) {
const fileInfo = await FileSystem.getInfoAsync(downloadsDir + file);
if (fileInfo.isDirectory) continue;
if (!file.endsWith(".json")) continue;
// const downloadsDir = Platform.select({
// ios: FileSystem.documentDirectory + "downloads/",
// android: FileSystem.cacheDirectory + "../files/downloads/",
// });
const fileContent = await FileSystem.readAsStringAsync(downloadsDir + file);
// if (!downloadsDir) throw new Error("Downloads directory not found");
// Check that fileContent is actually DownloadMetadata
if (!fileContent) continue;
if (!fileContent.includes("mediaSource")) continue;
if (!fileContent.includes("item")) continue;
// const dirInfo = await FileSystem.getInfoAsync(downloadsDir);
downloaded.push({
id: file.replace(".json", ""),
path: downloadsDir + file.replace(".json", ""),
metadata: JSON.parse(fileContent) as DownloadMetadata,
});
}
return downloaded;
// if (!dirInfo.exists) {
// console.warn("Downloads directory does not exist");
// return [];
// }
// const files = await FileSystem.readDirectoryAsync(downloadsDir);
// console.log("getDownloadedFiles ~", files.length);
// for (let file of files) {
// console.log(file);
// const fileInfo = await FileSystem.getInfoAsync(downloadsDir + file);
// if (fileInfo.isDirectory) continue;
// if (!file.endsWith(".json")) continue;
// const fileContent = await FileSystem.readAsStringAsync(downloadsDir + file);
// // Check that fileContent is actually DownloadMetadata
// if (!fileContent) continue;
// if (!fileContent.includes("mediaSource")) continue;
// if (!fileContent.includes("item")) continue;
// downloaded.push({
// id: file.replace(".json", ""),
// path: downloadsDir + file.replace(".json", ""),
// metadata: JSON.parse(fileContent) as DownloadMetadata,
// });
// }
// return downloaded;
};
const getDownloadedFile = async (id: string) => {
@@ -167,6 +188,12 @@ export const NativeDownloadProvider: React.FC<{
useEffect(() => {
const progressListener = addProgressListener((download) => {
console.log("p ~", {
id: download.id,
progress: download.progress,
state: download.state,
taskId: download.taskId,
});
if (!download.metadata) throw new Error("No metadata found in download");
setDownloads((prev) => ({