lots of downloads work

This commit is contained in:
Violet Caulfield
2026-02-14 11:06:55 -06:00
parent 5eabd83120
commit c616efa6f3
17 changed files with 153 additions and 56 deletions

View File

@@ -56,7 +56,7 @@
"react-native-url-polyfill": "3.0.0",
"react-native-uuid": "^2.0.3",
"react-native-worklets": "0.7.2",
"react-native-worklets-core": "^1.6.2",
"react-native-worklets-core": "1.6.3",
"ruby": "^0.6.1",
"scheduler": "^0.26.0",
"tamagui": "1.144.3",
@@ -1952,7 +1952,7 @@
"react-native-worklets": ["react-native-worklets@0.7.2", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog=="],
"react-native-worklets-core": ["react-native-worklets-core@1.6.2", "", { "dependencies": { "string-hash-64": "^1.0.3" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-zw73JfL40ZL/OD2TOil1El4D9ZwS3l6AFPeFfUWXh+V2/dHN8i28jHX8QXlz5DYtAkR+Ju3U1h4yiaODi/igZw=="],
"react-native-worklets-core": ["react-native-worklets-core@1.6.3", "", { "dependencies": { "string-hash-64": "^1.0.3" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r3Q40XQBccx/iAI5tlyiua+micvO1UGzzUOskNweZUXyfrrE+rb5aqxqruBPqXf90rO+bBiplylLMEAXCLTyGA=="],
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],

View File

@@ -2323,7 +2323,7 @@ PODS:
- SocketRocket
- Yoga
- react-native-vector-icons-material-design-icons (12.4.0)
- react-native-worklets-core (1.6.2):
- react-native-worklets-core (1.6.3):
- boost
- DoubleConversion
- fast_float
@@ -3704,7 +3704,7 @@ SPEC CHECKSUMS:
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
react-native-turbo-image: be4fdfeeced611d8d089d5ef879601d8bb293007
react-native-vector-icons-material-design-icons: 76cd460b3540b80527b4a80fb7f867f7deedb498
react-native-worklets-core: 28a6e2121dcf62543b703e81bc4860e9a0150cee
react-native-worklets-core: aaaac7d17f7e576592369a54f30e96fe4875c983
React-NativeModulesApple: a2c3d2cbec893956a5b3e4060322db2984fff75b
React-networking: 3f98bd96893a294376e7e03730947a08d474c380
React-oscompat: 80166b66da22e7af7fad94474e9997bd52d4c8c6

View File

@@ -89,7 +89,7 @@
"react-native-url-polyfill": "3.0.0",
"react-native-uuid": "^2.0.3",
"react-native-worklets": "0.7.2",
"react-native-worklets-core": "^1.6.2",
"react-native-worklets-core": "1.6.3",
"ruby": "^0.6.1",
"scheduler": "^0.26.0",
"tamagui": "1.144.3",

View File

@@ -23,3 +23,62 @@ index 37568c5..4ab42ca 100644
return true
}
diff --git a/node_modules/react-native-nitro-player/ios/download/DownloadFileManager.swift b/node_modules/react-native-nitro-player/ios/download/DownloadFileManager.swift
index 2654fc6..6b6ce4a 100644
--- a/node_modules/react-native-nitro-player/ios/download/DownloadFileManager.swift
+++ b/node_modules/react-native-nitro-player/ios/download/DownloadFileManager.swift
@@ -56,20 +56,28 @@ final class DownloadFileManager {
func saveDownloadedFile(
from temporaryLocation: URL, trackId: String, storageLocation: StorageLocation,
- originalURL: String? = nil
+ originalURL: String? = nil,
+ suggestedFilename: String? = nil
) -> String? {
print("🎯 DownloadFileManager: saveDownloadedFile called for trackId=\(trackId)")
print(" From: \(temporaryLocation.path)")
print(" Original URL: \(originalURL ?? "nil")")
+ print(" Suggested Filename: \(suggestedFilename ?? "nil")")
let destinationDirectory =
storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
print(" Destination directory: \(destinationDirectory.path)")
- // Determine file extension from the original URL, not the temp file
- // The temp file has .tmp extension which AVPlayer cannot play
+ // Determine file extension
var fileExtension = "mp3" // Default fallback
- if let originalURL = originalURL, let url = URL(string: originalURL) {
+
+ if let suggestedFilename = suggestedFilename, !suggestedFilename.isEmpty {
+ let url = URL(fileURLWithPath: suggestedFilename)
+ let pathExtension = url.pathExtension.lowercased()
+ if !pathExtension.isEmpty {
+ fileExtension = pathExtension
+ }
+ } else if let originalURL = originalURL, let url = URL(string: originalURL) {
let pathExtension = url.pathExtension.lowercased()
if !pathExtension.isEmpty {
fileExtension = pathExtension
diff --git a/node_modules/react-native-nitro-player/ios/download/DownloadManagerCore.swift b/node_modules/react-native-nitro-player/ios/download/DownloadManagerCore.swift
index d813fe2..63a256d 100644
--- a/node_modules/react-native-nitro-player/ios/download/DownloadManagerCore.swift
+++ b/node_modules/react-native-nitro-player/ios/download/DownloadManagerCore.swift
@@ -681,11 +681,16 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
let (storageLocation, originalURL) = queue.sync {
(self.config.storageLocation ?? .private, self.trackMetadata[trackId]?.url)
}
+
+ // Get suggested filename from response
+ let suggestedFilename = downloadTask.response?.suggestedFilename
+
let destinationPath = DownloadFileManager.shared.saveDownloadedFile(
from: location,
trackId: trackId,
storageLocation: storageLocation,
- originalURL: originalURL
+ originalURL: originalURL,
+ suggestedFilename: suggestedFilename
)
// Now handle the rest asynchronously

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/react-native-worklets-core/android/CMakeLists.txt b/node_modules/react-native-worklets-core/android/CMakeLists.txt
index cc4cbfe..61e1b56 100644
--- a/node_modules/react-native-worklets-core/android/CMakeLists.txt
+++ b/node_modules/react-native-worklets-core/android/CMakeLists.txt
@@ -81,7 +81,7 @@ if(${JS_RUNTIME} STREQUAL "hermes")
target_link_libraries(
${PACKAGE_NAME}
- hermes-engine::libhermes
+ hermes-engine::hermesvm
)
if(${HERMES_ENABLE_DEBUGGER})

View File

@@ -14,12 +14,13 @@ import { getApi } from '../../stores'
import { QueryKeys } from '../../enums/query-keys'
import { fetchAlbumDiscs } from '../../api/queries/item'
import { useQuery } from '@tanstack/react-query'
import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network/downloads'
import AlbumTrackListFooter from './footer'
import AlbumTrackListHeader from './header'
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
import { useStorageContext } from '../../providers/Storage'
import useIsDownloaded from '../../hooks/downloads'
import { useIsDownloaded } from '../../hooks/downloads'
import useDownloadTracks from '../../hooks/downloads/mutations'
import { useDownloadProgress } from 'react-native-nitro-player'
/**
* The screen for an Album's track list
@@ -45,11 +46,7 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
queryFn: () => fetchAlbumDiscs(api, album),
})
const isDownloaded = useIsDownloaded(
discs?.flatMap(({ data }) => data).map(({ Id }) => Id) ?? [],
)
const addToDownloadQueue = useAddToPendingDownloads()
const downloadTracks = useDownloadTracks()
const sections = (Array.isArray(discs) ? discs : []).map(({ title, data }) => ({
title,
@@ -60,13 +57,13 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
const albumTrackList = discs?.flatMap((disc) => disc.data)
const albumDownloadPending = useIsDownloading(albumTrackList ?? [])
const isDownloaded = useIsDownloaded(albumTrackList?.map(({ Id }) => Id) ?? [])
const { deleteDownloads } = useStorageContext()
const handleDeleteDownload = () => deleteDownloads(albumTrackList?.map(({ Id }) => Id!) ?? [])
const handleDownload = () => addToDownloadQueue(albumTrackList ?? [])
const handleDownload = () => downloadTracks.mutate(albumTrackList ?? [])
useLayoutEffect(() => {
navigation.setOptions({
@@ -85,7 +82,7 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
onPress={handleDeleteDownload}
/>
</Animated.View>
) : albumDownloadPending ? (
) : downloadTracks.isPending ? (
<Spinner justifyContent='center' color={'$neutral'} />
) : (
<Animated.View
@@ -110,7 +107,7 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
isDownloaded,
handleDeleteDownload,
handleDownload,
albumDownloadPending,
downloadTracks.isPending,
])
return (

View File

@@ -28,8 +28,8 @@ import { triggerHaptic } from '../../hooks/use-haptic-feedback'
import { Platform } from 'react-native'
import { useApi } from '../../stores'
import DeletePlaylistRow from './components/delete-playlist-row'
import { DownloadManager, useDownloadProgress } from 'react-native-nitro-player'
import useDownloadTracks, { useDeleteDownloads } from '../../hooks/downloads/mutations'
import { useIsDownloaded } from '../../hooks/downloads'
type StackNavigation = Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
@@ -230,14 +230,7 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
function DownloadMenuRow({ items }: { items: BaseItemDto[] }): React.JSX.Element {
const { isPending, mutate: download } = useDownloadTracks()
const progress = useDownloadProgress({ trackIds: items.map((item) => item.Id!) })
const isDownloaded =
items.filter((item) =>
DownloadManager.getAllDownloadedTracks()
.map((download) => download.trackId)
.includes(item.Id!),
).length === items.length
const isDownloaded = useIsDownloaded(items.map((item) => item.Id))
const removeDownloads = useDeleteDownloads()

View File

@@ -19,7 +19,7 @@ import { useAddFavorite, useRemoveFavorite } from '../../../../api/mutations/fav
import { StackActions } from '@react-navigation/native'
import { useHideRunTimesSetting } from '../../../../stores/settings/app'
import TrackRowContent from './content'
import useIsDownloaded from '../../../../hooks/downloads'
import { useIsDownloaded } from '../../../../hooks/downloads'
export interface TrackProps {
track: BaseItemDto

View File

@@ -1,7 +1,7 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import Icon from './icon'
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
import useIsDownloaded from '../../../hooks/downloads'
import { useIsDownloaded } from '../../../hooks/downloads'
function DownloadedIcon({ item }: { item: BaseItemDto }) {
const isDownloaded = useIsDownloaded([item.Id])

View File

@@ -4,7 +4,7 @@ import navigationRef from '../../../../navigation'
import { parseBitrateFromTranscodingUrl } from '../../../utils/parsing/url'
import { BaseItemDto, MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client'
import { BUTTON_PRESS_STYLES } from '../../../configs/style.config'
import { useDownloadProgress } from 'react-native-nitro-player'
import { useIsDownloaded } from '../../../hooks/downloads'
interface QualityBadgeProps {
item: BaseItemDto
@@ -22,7 +22,7 @@ export default function QualityBadge({
? parseBitrateFromTranscodingUrl(transcodingUrl)
: mediaSourceInfo.Bitrate
const isDownloaded = useDownloadProgress({ trackIds: [item.Id!] }).overallProgress === 1
const isDownloaded = useIsDownloaded([item.Id])
return bitrate && container ? (
<Square

View File

@@ -35,7 +35,7 @@ import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network
import { useStorageContext } from '../../providers/Storage'
import { queryClient } from '../../constants/query-client'
import { PlaylistTracksQueryKey } from '../../api/queries/playlist/keys'
import useIsDownloaded from '../../hooks/downloads'
import { useIsDownloaded } from '../../hooks/downloads'
export default function Playlist({
playlist,

View File

@@ -1,13 +1,14 @@
import { useDownloadedTracks } from 'react-native-nitro-player'
import { useQuery } from '@tanstack/react-query'
import ALL_DOWNLOADS_QUERY from './queries'
const useIsDownloaded = (trackIds: (string | null | undefined)[]) => {
const { downloadedTracks } = useDownloadedTracks()
const useDownloads = () => useQuery(ALL_DOWNLOADS_QUERY)
const downloadedTrackIds = new Set(
downloadedTracks.map((download) => download.originalTrack.id),
export const useIsDownloaded = (trackIds: (string | null | undefined)[]) => {
const { data: downloadedTracks } = useQuery(ALL_DOWNLOADS_QUERY)
return trackIds.every((id) =>
downloadedTracks?.some((download) => download.originalTrack.id === id),
)
return trackIds.every((id) => id != null && downloadedTrackIds.has(id))
}
export default useIsDownloaded
export default useDownloads

View File

@@ -0,0 +1,3 @@
const ALL_DOWNLOADS_KEY = ['ALL_DOWNLOADS'] as const
export default ALL_DOWNLOADS_KEY

View File

@@ -3,9 +3,10 @@ import { mapDtoToTrack } from '../../utils/mapping/item-to-track'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import { useMutation } from '@tanstack/react-query'
import { DownloadManager, PlayerQueue } from 'react-native-nitro-player'
import { refetchDownloadsAfterDelay } from './utils'
const useDownloadTracks = () =>
useMutation({
const useDownloadTracks = () => {
const download = useMutation({
mutationFn: async (items: BaseItemDto[]) => {
const { deviceProfile } = useDownloadingDeviceProfileStore.getState()
@@ -18,15 +19,31 @@ const useDownloadTracks = () =>
PlayerQueue.addTracksToPlaylist(playlistId, tracks)
await DownloadManager.downloadPlaylist(playlistId, tracks)
// Refetch downloads after a delay to allow the new download to be registered
refetchDownloadsAfterDelay()
},
})
export const useDeleteDownloads = () =>
useMutation({
return {
mutate: download.mutate,
isPending: download.isPending,
}
}
export const useDeleteDownloads = () => {
const deleteDownloads = useMutation({
mutationFn: async (items: BaseItemDto[]) => {
const trackIds = items.map((item) => item.Id)
await Promise.all(trackIds.map((id) => DownloadManager.deleteDownloadedTrack(id!)))
},
onSettled: refetchDownloadsAfterDelay,
})
return {
mutate: deleteDownloads.mutate,
isPending: deleteDownloads.isPending,
}
}
export default useDownloadTracks

View File

@@ -0,0 +1,9 @@
import { DownloadManager } from 'react-native-nitro-player'
import ALL_DOWNLOADS_KEY from './keys'
const ALL_DOWNLOADS_QUERY = {
queryKey: ALL_DOWNLOADS_KEY,
queryFn: () => DownloadManager.getAllDownloadedTracks(),
}
export default ALL_DOWNLOADS_QUERY

View File

@@ -0,0 +1,8 @@
import { queryClient } from '../../constants/query-client'
import ALL_DOWNLOADS_KEY from './keys'
export function refetchDownloadsAfterDelay() {
setTimeout(() => {
queryClient.refetchQueries({ queryKey: ALL_DOWNLOADS_KEY })
}, 3000) // Refetch downloads after a delay to allow the new download to be registered
}

View File

@@ -8,6 +8,7 @@ import { setPlaybackPosition, usePlayerPlaybackStore } from '../../../stores/pla
import { useUsageSettingsStore } from '../../../stores/settings/usage'
import isPlaybackFinished from '../../../api/mutations/playback/utils'
import reportPlaybackCompleted from '../../../api/mutations/playback/functions/playback-completed'
import { refetchDownloadsAfterDelay } from '../../../hooks/downloads/utils'
export default function Initialize() {
restoreFromStorage()
@@ -55,8 +56,30 @@ function registerEventHandlers() {
}
})
TrackPlayer.onPlaybackProgressChange(async (position) => {
TrackPlayer.onPlaybackProgressChange(async (position, totalDuration) => {
setPlaybackPosition(position)
const currentTrack = usePlayerQueueStore.getState().currentTrack
const autoDownload = useUsageSettingsStore.getState().autoDownload
const isDownloadedOrDownloadPending =
(await DownloadManager.isTrackDownloaded(currentTrack?.id ?? '')) ||
(await DownloadManager.isDownloading(currentTrack?.id ?? ''))
if (
position / totalDuration > 0.3 &&
currentTrack &&
autoDownload &&
!isDownloadedOrDownloadPending
) {
try {
await DownloadManager.downloadTrack(currentTrack)
refetchDownloadsAfterDelay()
} catch (error) {
console.warn('Error auto-downloading track:', error)
}
}
})
}