mirror of
https://github.com/Jellify-Music/App.git
synced 2026-03-17 18:51:24 -05:00
lots of downloads work
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
3
src/hooks/downloads/keys.ts
Normal file
3
src/hooks/downloads/keys.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const ALL_DOWNLOADS_KEY = ['ALL_DOWNLOADS'] as const
|
||||
|
||||
export default ALL_DOWNLOADS_KEY
|
||||
@@ -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
|
||||
|
||||
9
src/hooks/downloads/queries.ts
Normal file
9
src/hooks/downloads/queries.ts
Normal 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
|
||||
8
src/hooks/downloads/utils.ts
Normal file
8
src/hooks/downloads/utils.ts
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user