mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-03 13:56:29 +01:00
fix: working state updates for active/non-active downloads
This commit is contained in:
@@ -100,7 +100,8 @@ export default function Index() {
|
|||||||
const deleteFile = async (id: string) => {
|
const deleteFile = async (id: string) => {
|
||||||
const downloadsDir = FileSystem.documentDirectory + "downloads/";
|
const downloadsDir = FileSystem.documentDirectory + "downloads/";
|
||||||
await FileSystem.deleteAsync(downloadsDir + id + ".json");
|
await FileSystem.deleteAsync(downloadsDir + id + ".json");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["downloadedFiles"] });
|
await FileSystem.deleteAsync(downloadsDir + id);
|
||||||
|
refetchDownloadedFiles()
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
// Separate delegate class for managing download-specific state
|
|
||||||
class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
||||||
weak var module: HlsDownloaderModule?
|
weak var module: HlsDownloaderModule?
|
||||||
var taskIdentifier: Int = 0
|
var taskIdentifier: Int = 0
|
||||||
@@ -40,12 +40,10 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class HlsDownloaderModule: Module {
|
public class HlsDownloaderModule: Module {
|
||||||
// Main delegate handler for the download session
|
|
||||||
private lazy var delegateHandler: HLSDownloadDelegate = {
|
private lazy var delegateHandler: HLSDownloadDelegate = {
|
||||||
return HLSDownloadDelegate(module: self)
|
return HLSDownloadDelegate(module: self)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Track active downloads with all necessary information
|
|
||||||
var activeDownloads:
|
var activeDownloads:
|
||||||
[Int: (
|
[Int: (
|
||||||
task: AVAssetDownloadTask,
|
task: AVAssetDownloadTask,
|
||||||
@@ -54,7 +52,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
startTime: Double
|
startTime: Double
|
||||||
)] = [:]
|
)] = [:]
|
||||||
|
|
||||||
// Configure background download session
|
|
||||||
private lazy var downloadSession: AVAssetDownloadURLSession = {
|
private lazy var downloadSession: AVAssetDownloadURLSession = {
|
||||||
let configuration = URLSessionConfiguration.background(
|
let configuration = URLSessionConfiguration.background(
|
||||||
withIdentifier: "com.example.hlsdownload")
|
withIdentifier: "com.example.hlsdownload")
|
||||||
@@ -73,6 +70,20 @@ public class HlsDownloaderModule: Module {
|
|||||||
|
|
||||||
Events("onProgress", "onError", "onComplete")
|
Events("onProgress", "onError", "onComplete")
|
||||||
|
|
||||||
|
// Function("requestNotificationPermission") { () -> Bool in
|
||||||
|
// var permissionGranted = false
|
||||||
|
// let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
|
// UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
|
||||||
|
// granted, error in
|
||||||
|
// permissionGranted = granted
|
||||||
|
// semaphore.signal()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// _ = semaphore.wait(timeout: .now() + 5.0)
|
||||||
|
// return permissionGranted
|
||||||
|
// }
|
||||||
|
|
||||||
Function("getActiveDownloads") { () -> [[String: Any]] in
|
Function("getActiveDownloads") { () -> [[String: Any]] in
|
||||||
return activeDownloads.map { (taskId, downloadInfo) in
|
return activeDownloads.map { (taskId, downloadInfo) in
|
||||||
return [
|
return [
|
||||||
@@ -89,14 +100,12 @@ public class HlsDownloaderModule: Module {
|
|||||||
(providedId: String, url: String, metadata: [String: Any]?) -> Void in
|
(providedId: String, url: String, metadata: [String: Any]?) -> Void in
|
||||||
let startTime = Date().timeIntervalSince1970
|
let startTime = Date().timeIntervalSince1970
|
||||||
|
|
||||||
// Check if download already exists
|
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
let downloadsDir = docs.appendingPathComponent("downloads", isDirectory: true)
|
let downloadsDir = docs.appendingPathComponent("downloads", isDirectory: true)
|
||||||
let potentialExistingLocation = downloadsDir.appendingPathComponent(
|
let potentialExistingLocation = downloadsDir.appendingPathComponent(
|
||||||
providedId, isDirectory: true)
|
providedId, isDirectory: true)
|
||||||
|
|
||||||
// If download exists and is valid, return immediately
|
|
||||||
if fm.fileExists(atPath: potentialExistingLocation.path) {
|
if fm.fileExists(atPath: potentialExistingLocation.path) {
|
||||||
if let files = try? fm.contentsOfDirectory(atPath: potentialExistingLocation.path),
|
if let files = try? fm.contentsOfDirectory(atPath: potentialExistingLocation.path),
|
||||||
files.contains(where: { $0.hasSuffix(".m3u8") })
|
files.contains(where: { $0.hasSuffix(".m3u8") })
|
||||||
@@ -116,7 +125,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate URL
|
|
||||||
guard let assetURL = URL(string: url) else {
|
guard let assetURL = URL(string: url) else {
|
||||||
self.sendEvent(
|
self.sendEvent(
|
||||||
"onError",
|
"onError",
|
||||||
@@ -130,7 +138,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure asset with necessary options
|
|
||||||
let asset = AVURLAsset(
|
let asset = AVURLAsset(
|
||||||
url: assetURL,
|
url: assetURL,
|
||||||
options: [
|
options: [
|
||||||
@@ -139,7 +146,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
"AVURLAssetAllowsCellularAccessKey": true,
|
"AVURLAssetAllowsCellularAccessKey": true,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Load asset asynchronously
|
|
||||||
asset.loadValuesAsynchronously(forKeys: ["playable", "duration"]) {
|
asset.loadValuesAsynchronously(forKeys: ["playable", "duration"]) {
|
||||||
var error: NSError?
|
var error: NSError?
|
||||||
let status = asset.statusOfValue(forKey: "playable", error: &error)
|
let status = asset.statusOfValue(forKey: "playable", error: &error)
|
||||||
@@ -159,7 +165,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create download task with quality options
|
|
||||||
guard
|
guard
|
||||||
let task = self.downloadSession.makeAssetDownloadTask(
|
let task = self.downloadSession.makeAssetDownloadTask(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
@@ -185,16 +190,13 @@ public class HlsDownloaderModule: Module {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure delegate for this download
|
|
||||||
let delegate = HLSDownloadDelegate(module: self)
|
let delegate = HLSDownloadDelegate(module: self)
|
||||||
delegate.providedId = providedId
|
delegate.providedId = providedId
|
||||||
delegate.startTime = startTime
|
delegate.startTime = startTime
|
||||||
delegate.taskIdentifier = task.taskIdentifier
|
delegate.taskIdentifier = task.taskIdentifier
|
||||||
|
|
||||||
// Store download information
|
|
||||||
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:], startTime)
|
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:], startTime)
|
||||||
|
|
||||||
// Send initial progress event
|
|
||||||
self.sendEvent(
|
self.sendEvent(
|
||||||
"onProgress",
|
"onProgress",
|
||||||
[
|
[
|
||||||
@@ -205,13 +207,11 @@ public class HlsDownloaderModule: Module {
|
|||||||
"startTime": startTime,
|
"startTime": startTime,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Start the download
|
|
||||||
task.resume()
|
task.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional methods and event handlers...
|
|
||||||
Function("cancelDownload") { (providedId: String) -> Void in
|
Function("cancelDownload") { (providedId: String) -> Void in
|
||||||
guard
|
guard
|
||||||
let entry = self.activeDownloads.first(where: { $0.value.delegate.providedId == providedId }
|
let entry = self.activeDownloads.first(where: { $0.value.delegate.providedId == providedId }
|
||||||
@@ -237,11 +237,30 @@ public class HlsDownloaderModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
func removeDownload(with id: Int) {
|
func removeDownload(with id: Int) {
|
||||||
activeDownloads.removeValue(forKey: id)
|
activeDownloads.removeValue(forKey: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sendDownloadCompletionNotification(title: String, body: String) {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = title
|
||||||
|
content.body = body
|
||||||
|
content.sound = .default
|
||||||
|
|
||||||
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||||
|
let request = UNNotificationRequest(
|
||||||
|
identifier: UUID().uuidString,
|
||||||
|
content: content,
|
||||||
|
trigger: trigger
|
||||||
|
)
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
print("Error showing notification: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func persistDownloadedFolder(originalLocation: URL, folderName: String) throws -> URL {
|
func persistDownloadedFolder(originalLocation: URL, folderName: String) throws -> URL {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
@@ -270,7 +289,6 @@ public class HlsDownloaderModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension for URL session delegate methods
|
|
||||||
extension HlsDownloaderModule {
|
extension HlsDownloaderModule {
|
||||||
func urlSession(
|
func urlSession(
|
||||||
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange,
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange,
|
||||||
@@ -324,6 +342,21 @@ extension HlsDownloaderModule {
|
|||||||
do {
|
do {
|
||||||
try await rewriteM3U8Files(baseDir: newLocation.path)
|
try await rewriteM3U8Files(baseDir: newLocation.path)
|
||||||
|
|
||||||
|
// Safely access metadata for notification
|
||||||
|
let notificationBody: String
|
||||||
|
if let item = downloadInfo.metadata["item"] as? [String: Any],
|
||||||
|
let name = item["Name"] as? String
|
||||||
|
{
|
||||||
|
notificationBody = "\(name) has finished downloading."
|
||||||
|
} else {
|
||||||
|
notificationBody = "Download completed successfully."
|
||||||
|
}
|
||||||
|
|
||||||
|
sendDownloadCompletionNotification(
|
||||||
|
title: "Download Complete",
|
||||||
|
body: notificationBody
|
||||||
|
)
|
||||||
|
|
||||||
sendEvent(
|
sendEvent(
|
||||||
"onComplete",
|
"onComplete",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -20,9 +20,17 @@ import {
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { apiAtom, userAtom } from "./JellyfinProvider";
|
import { apiAtom, userAtom } from "./JellyfinProvider";
|
||||||
|
import { useFocusEffect } from "expo-router";
|
||||||
|
import { AppState, AppStateStatus } from "react-native";
|
||||||
|
|
||||||
type DownloadOptionsData = {
|
type DownloadOptionsData = {
|
||||||
selectedAudioStream: number;
|
selectedAudioStream: number;
|
||||||
@@ -120,38 +128,47 @@ export const NativeDownloadProvider: React.FC<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _getActiveDownloads = async () => {
|
const handleAppStateChange = (state: AppStateStatus) => {
|
||||||
const activeDownloads = await getActiveDownloads();
|
if (state === "background" || state === "inactive") {
|
||||||
setDownloads((prev) => {
|
setDownloads({});
|
||||||
const newDownloads = { ...prev };
|
} else if (state === "active") {
|
||||||
activeDownloads.forEach((download) => {
|
const _getActiveDownloads = async () => {
|
||||||
newDownloads[download.id] = {
|
const activeDownloads = await getActiveDownloads();
|
||||||
id: download.id,
|
setDownloads((prev) => {
|
||||||
progress: download.progress,
|
const newDownloads = { ...prev };
|
||||||
state: download.state,
|
activeDownloads.forEach((download) => {
|
||||||
secondsDownloaded: download.secondsDownloaded,
|
newDownloads[download.id] = {
|
||||||
secondsTotal: download.secondsTotal,
|
id: download.id,
|
||||||
metadata: download.metadata,
|
progress: download.progress,
|
||||||
startTime: download.startTime,
|
state: download.state,
|
||||||
};
|
secondsDownloaded: download.secondsDownloaded,
|
||||||
});
|
secondsTotal: download.secondsTotal,
|
||||||
return newDownloads;
|
metadata: download.metadata,
|
||||||
});
|
startTime: download.startTime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return newDownloads;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
_getActiveDownloads();
|
||||||
|
refetchDownloadedFiles();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_getActiveDownloads();
|
const subscription = AppState.addEventListener(
|
||||||
|
"change",
|
||||||
|
handleAppStateChange
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.remove();
|
||||||
|
};
|
||||||
|
}, [getActiveDownloads]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const progressListener = addProgressListener((download) => {
|
const progressListener = addProgressListener((download) => {
|
||||||
if (!download.metadata) throw new Error("No metadata found in download");
|
if (!download.metadata) throw new Error("No metadata found in download");
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[HLS] Download progress:",
|
|
||||||
download.metadata.item.Id,
|
|
||||||
download.progress,
|
|
||||||
download.state,
|
|
||||||
download.taskId
|
|
||||||
);
|
|
||||||
|
|
||||||
setDownloads((prev) => ({
|
setDownloads((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[download.id]: {
|
[download.id]: {
|
||||||
|
|||||||
Reference in New Issue
Block a user