mirror of
https://github.com/Jellify-Music/App.git
synced 2026-04-26 04:49:27 -05:00
Slimify JellifyTrack type (#644)
* Slimify JellifyTrack type Reduces the overall size of a JellifyTrack object in memory with the goal of lowing the overhead on serialization both to native and to zustand * simplify addToQueue fix issue where using the swipeable row would cause a crash
This commit is contained in:
@@ -2,8 +2,6 @@ import TrackPlayer from 'react-native-track-player'
|
||||
import { playLaterInQueue } from '../../src/providers/Player/functions/queue'
|
||||
import { BaseItemDto, DeviceProfile } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { JellifyDownload } from '@/src/types/JellifyDownload'
|
||||
import { TrackType } from 'react-native-track-player'
|
||||
|
||||
describe('Add to Queue - playLaterInQueue', () => {
|
||||
it('adds track to the end of the queue', async () => {
|
||||
@@ -20,28 +18,12 @@ describe('Add to Queue - playLaterInQueue', () => {
|
||||
const api: Partial<Api> = { basePath: '' }
|
||||
const deviceProfile: Partial<DeviceProfile> = { Name: 'test' }
|
||||
|
||||
const downloaded: JellifyDownload = {
|
||||
// Minimal viable JellifyTrack fields
|
||||
url: '/downloads/t1.mp3',
|
||||
duration: 180,
|
||||
type: TrackType.Default,
|
||||
item: track,
|
||||
sessionId: null,
|
||||
sourceType: 'download',
|
||||
artwork: '/downloads/t1.jpg',
|
||||
// JellifyDownload fields
|
||||
savedAt: new Date().toISOString(),
|
||||
isAutoDownloaded: false,
|
||||
path: '/downloads/t1.mp3',
|
||||
}
|
||||
|
||||
await playLaterInQueue({
|
||||
api: api as Api,
|
||||
deviceProfile: deviceProfile as DeviceProfile,
|
||||
networkStatus: null,
|
||||
tracks: [track],
|
||||
queuingType: undefined,
|
||||
downloadedTracks: [downloaded],
|
||||
})
|
||||
|
||||
expect(TrackPlayer.add).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -40,7 +40,7 @@ export const useDownloadAudioItem: () => [
|
||||
)
|
||||
return Promise.resolve(false)
|
||||
|
||||
const track = mapDtoToTrack(api, item, downloadedTracks ?? [], deviceProfile)
|
||||
const track = mapDtoToTrack(api, item, deviceProfile)
|
||||
|
||||
return saveAudio(track, setDownloadProgress, autoCached)
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async function saveAudioItem(
|
||||
if (downloadedTracks?.filter((download) => download.item.Id === item.Id).length ?? 0 > 0)
|
||||
return Promise.resolve(false)
|
||||
|
||||
const track = mapDtoToTrack(api, item, downloadedTracks ?? [], deviceProfile)
|
||||
const track = mapDtoToTrack(api, item, deviceProfile)
|
||||
|
||||
// TODO: fix download progresses
|
||||
return saveAudio(track, () => {}, autoCached)
|
||||
|
||||
@@ -59,8 +59,8 @@ const useTracks: () => [
|
||||
return (downloadedTracks ?? [])
|
||||
.map(({ item }) => item)
|
||||
.sort((a, b) => {
|
||||
if ((a.Name ?? '') < (b.Name ?? '')) return -1
|
||||
else if ((a.Name ?? '') === (b.Name ?? '')) return 0
|
||||
if ((a.SortName ?? '') < (b.SortName ?? '')) return -1
|
||||
else if ((a.SortName ?? '') === (b.SortName ?? '')) return 0
|
||||
else return 1
|
||||
})
|
||||
.filter((track) => {
|
||||
|
||||
@@ -46,9 +46,7 @@ export function Album(): React.JSX.Element {
|
||||
|
||||
const downloadAlbum = (item: BaseItemDto[]) => {
|
||||
if (!api) return
|
||||
const jellifyTracks = item.map((item) =>
|
||||
mapDtoToTrack(api, item, [], downloadingDeviceProfile),
|
||||
)
|
||||
const jellifyTracks = item.map((item) => mapDtoToTrack(api, item, downloadingDeviceProfile))
|
||||
addToDownloadQueue(jellifyTracks)
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const { mutate: addToQueue } = useAddToQueue()
|
||||
const addToQueue = useAddToQueue()
|
||||
|
||||
const mutation: AddToQueueMutation = {
|
||||
api,
|
||||
@@ -210,8 +210,8 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
|
||||
flex={1}
|
||||
gap={'$2.5'}
|
||||
justifyContent='flex-start'
|
||||
onPress={() => {
|
||||
addToQueue({
|
||||
onPress={async () => {
|
||||
await addToQueue({
|
||||
...mutation,
|
||||
queuingType: QueuingType.PlayingNext,
|
||||
})
|
||||
@@ -258,7 +258,7 @@ function DownloadMenuRow({ items }: { items: BaseItemDto[] }): React.JSX.Element
|
||||
const downloadItems = useCallback(() => {
|
||||
if (!api) return
|
||||
|
||||
const tracks = items.map((item) => mapDtoToTrack(api, item, [], deviceProfile))
|
||||
const tracks = items.map((item) => mapDtoToTrack(api, item, deviceProfile))
|
||||
addToDownloadQueue(tracks)
|
||||
}, [addToDownloadQueue, items])
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import Animated, {
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import { runOnJS } from 'react-native-worklets'
|
||||
import Icon from './icon'
|
||||
import { Text } from '../helpers/text'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
registerSwipeableRow,
|
||||
unregisterSwipeableRow,
|
||||
} from './swipeable-row-registry'
|
||||
import { runOnJS, runOnUISync, scheduleOnRN, scheduleOnUI } from 'react-native-worklets'
|
||||
|
||||
export type SwipeAction = {
|
||||
label: string
|
||||
@@ -55,8 +55,8 @@ export default function SwipeableRow({
|
||||
}: Props) {
|
||||
const triggerHaptic = useHapticFeedback()
|
||||
const tx = useSharedValue(0)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const menuOpen = useSharedValue(false)
|
||||
const dragging = useSharedValue(false)
|
||||
const idRef = useRef<string | undefined>(undefined)
|
||||
const menuOpenRef = useRef(false)
|
||||
const defaultMaxLeft = 120
|
||||
@@ -86,8 +86,9 @@ export default function SwipeableRow({
|
||||
}
|
||||
|
||||
const syncClosedState = useCallback(() => {
|
||||
'worklet'
|
||||
menuOpenRef.current = false
|
||||
setMenuOpen(false)
|
||||
menuOpen.set(false)
|
||||
notifySwipeableRowClosed(idRef.current!)
|
||||
}, [])
|
||||
|
||||
@@ -98,7 +99,7 @@ export default function SwipeableRow({
|
||||
|
||||
const openMenu = useCallback(() => {
|
||||
menuOpenRef.current = true
|
||||
setMenuOpen(true)
|
||||
menuOpen.set(true)
|
||||
notifySwipeableRowOpened(idRef.current!)
|
||||
}, [])
|
||||
|
||||
@@ -110,10 +111,11 @@ export default function SwipeableRow({
|
||||
}, [close])
|
||||
|
||||
useEffect(() => {
|
||||
menuOpenRef.current = menuOpen
|
||||
menuOpenRef.current = menuOpen.value
|
||||
}, [menuOpen])
|
||||
|
||||
const schedule = (fn?: () => void) => {
|
||||
'worklet'
|
||||
if (!fn) return
|
||||
// Defer JS work so the UI bounce plays smoothly
|
||||
setTimeout(() => fn(), 0)
|
||||
@@ -121,11 +123,12 @@ export default function SwipeableRow({
|
||||
|
||||
const gesture = useMemo(() => {
|
||||
return Gesture.Pan()
|
||||
.runOnJS(true)
|
||||
.activeOffsetX([-10, 10])
|
||||
.failOffsetY([-10, 10])
|
||||
.onBegin(() => {
|
||||
if (disabled) return
|
||||
runOnJS(setDragging)(true)
|
||||
dragging.set(true)
|
||||
})
|
||||
.onUpdate((e) => {
|
||||
if (disabled) return
|
||||
@@ -137,21 +140,21 @@ export default function SwipeableRow({
|
||||
if (tx.value > threshold) {
|
||||
// Right swipe: show left quick actions if provided; otherwise trigger leftAction
|
||||
if (leftActions && leftActions.length > 0) {
|
||||
runOnJS(triggerHaptic)('impactLight')
|
||||
triggerHaptic('impactLight')
|
||||
// Snap open to expose quick actions, do not auto-trigger
|
||||
tx.value = withTiming(maxLeft, {
|
||||
duration: 140,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
})
|
||||
runOnJS(openMenu)()
|
||||
openMenu()
|
||||
return
|
||||
} else if (leftAction) {
|
||||
runOnJS(triggerHaptic)('impactLight')
|
||||
triggerHaptic('impactLight')
|
||||
tx.value = withTiming(
|
||||
maxLeft,
|
||||
{ duration: 140, easing: Easing.out(Easing.cubic) },
|
||||
() => {
|
||||
runOnJS(schedule)(leftAction.onTrigger)
|
||||
scheduleOnRN(leftAction.onTrigger)
|
||||
tx.value = withTiming(0, {
|
||||
duration: 160,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
@@ -164,21 +167,21 @@ export default function SwipeableRow({
|
||||
// Left swipe (quick actions)
|
||||
if (tx.value < -Math.min(threshold, Math.abs(maxRight) / 2)) {
|
||||
if (rightActions && rightActions.length > 0) {
|
||||
runOnJS(triggerHaptic)('impactLight')
|
||||
triggerHaptic('impactLight')
|
||||
// Snap open to expose quick actions, do not auto-trigger
|
||||
tx.value = withTiming(maxRight, {
|
||||
duration: 140,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
})
|
||||
runOnJS(openMenu)()
|
||||
openMenu()
|
||||
return
|
||||
} else if (rightAction) {
|
||||
runOnJS(triggerHaptic)('impactLight')
|
||||
triggerHaptic('impactLight')
|
||||
tx.value = withTiming(
|
||||
maxRight,
|
||||
{ duration: 140, easing: Easing.out(Easing.cubic) },
|
||||
() => {
|
||||
runOnJS(schedule)(rightAction.onTrigger)
|
||||
scheduleOnRN(rightAction.onTrigger)
|
||||
tx.value = withTiming(0, {
|
||||
duration: 160,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
@@ -189,11 +192,11 @@ export default function SwipeableRow({
|
||||
}
|
||||
}
|
||||
tx.value = withTiming(0, { duration: 160, easing: Easing.out(Easing.cubic) })
|
||||
runOnJS(syncClosedState)()
|
||||
syncClosedState()
|
||||
})
|
||||
.onFinalize(() => {
|
||||
if (disabled) return
|
||||
runOnJS(setDragging)(false)
|
||||
dragging.set(false)
|
||||
})
|
||||
}, [
|
||||
disabled,
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function ItemRow({
|
||||
const deviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const loadNewQueue = useLoadNewQueue()
|
||||
const { mutate: addToQueue } = useAddToQueue()
|
||||
const addToQueue = useAddToQueue()
|
||||
const { mutate: addFavorite } = useAddFavorite()
|
||||
const { mutate: removeFavorite } = useRemoveFavorite()
|
||||
|
||||
@@ -119,8 +119,8 @@ export default function ItemRow({
|
||||
|
||||
const swipeHandlers = useCallback(
|
||||
() => ({
|
||||
addToQueue: () =>
|
||||
addToQueue({
|
||||
addToQueue: async () =>
|
||||
await addToQueue({
|
||||
api,
|
||||
deviceProfile,
|
||||
networkStatus,
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function Track({
|
||||
const nowPlaying = useCurrentTrack()
|
||||
const playQueue = usePlayQueue()
|
||||
const loadNewQueue = useLoadNewQueue()
|
||||
const { mutate: addToQueue } = useAddToQueue()
|
||||
const addToQueue = useAddToQueue()
|
||||
const [networkStatus] = useNetworkStatus()
|
||||
|
||||
const { data: mediaInfo } = useStreamedMediaInfo(track.Id)
|
||||
@@ -172,19 +172,23 @@ export default function Track({
|
||||
|
||||
const swipeHandlers = useMemo(
|
||||
() => ({
|
||||
addToQueue: () =>
|
||||
addToQueue({
|
||||
addToQueue: async () => {
|
||||
console.info('Running add to queue swipe action')
|
||||
await addToQueue({
|
||||
api,
|
||||
deviceProfile,
|
||||
networkStatus,
|
||||
tracks: [track],
|
||||
queuingType: QueuingType.DirectlyQueued,
|
||||
}),
|
||||
})
|
||||
},
|
||||
toggleFavorite: () => {
|
||||
console.info(`Running ${isFavoriteTrack ? 'Remove' : 'Add'} favorite swipe action`)
|
||||
if (isFavoriteTrack) removeFavorite({ item: track })
|
||||
else addFavorite({ item: track })
|
||||
},
|
||||
addToPlaylist: () => {
|
||||
console.info('Running add to playlist swipe handler')
|
||||
navigationRef.dispatch(StackActions.push('AddToPlaylist', { track }))
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -142,7 +142,7 @@ function PlaylistHeaderControls({
|
||||
const downloadPlaylist = () => {
|
||||
if (!api) return
|
||||
const jellifyTracks = playlistTracks.map((item) =>
|
||||
mapDtoToTrack(api, item, [], downloadingDeviceProfile),
|
||||
mapDtoToTrack(api, item, downloadingDeviceProfile),
|
||||
)
|
||||
addToDownloadQueue(jellifyTracks)
|
||||
}
|
||||
|
||||
@@ -43,13 +43,7 @@ export async function loadQueue({
|
||||
|
||||
// Convert to JellifyTracks first
|
||||
let queue = availableAudioItems.map((item) =>
|
||||
mapDtoToTrack(
|
||||
api!,
|
||||
item,
|
||||
downloadedTracks ?? [],
|
||||
deviceProfile!,
|
||||
QueuingType.FromSelection,
|
||||
),
|
||||
mapDtoToTrack(api!, item, deviceProfile!, QueuingType.FromSelection),
|
||||
)
|
||||
|
||||
// Store the original unshuffled queue
|
||||
@@ -93,9 +87,6 @@ export async function loadQueue({
|
||||
}
|
||||
}
|
||||
|
||||
type PlayNextOperation = AddToQueueMutation & {
|
||||
downloadedTracks: JellifyDownload[] | undefined
|
||||
}
|
||||
/**
|
||||
* Inserts a track at the next index in the queue
|
||||
*
|
||||
@@ -103,14 +94,9 @@ type PlayNextOperation = AddToQueueMutation & {
|
||||
*
|
||||
* @param item The track to play next
|
||||
*/
|
||||
export const playNextInQueue = async ({
|
||||
api,
|
||||
downloadedTracks,
|
||||
deviceProfile,
|
||||
tracks,
|
||||
}: PlayNextOperation) => {
|
||||
export const playNextInQueue = async ({ api, deviceProfile, tracks }: AddToQueueMutation) => {
|
||||
const tracksToPlayNext = tracks.map((item) =>
|
||||
mapDtoToTrack(api!, item, downloadedTracks ?? [], deviceProfile!, QueuingType.PlayingNext),
|
||||
mapDtoToTrack(api!, item, deviceProfile!, QueuingType.PlayingNext),
|
||||
)
|
||||
|
||||
const currentIndex = await TrackPlayer.getActiveTrackIndex()
|
||||
@@ -146,26 +132,11 @@ export const playNextInQueue = async ({
|
||||
])
|
||||
}
|
||||
|
||||
type QueueOperation = AddToQueueMutation & {
|
||||
downloadedTracks: JellifyDownload[] | undefined
|
||||
}
|
||||
|
||||
export const playLaterInQueue = async ({
|
||||
api,
|
||||
deviceProfile,
|
||||
downloadedTracks,
|
||||
tracks,
|
||||
}: QueueOperation) => {
|
||||
export const playLaterInQueue = async ({ api, deviceProfile, tracks }: AddToQueueMutation) => {
|
||||
console.debug(`Adding ${tracks.length} to queue`)
|
||||
|
||||
const newTracks = tracks.map((item) =>
|
||||
mapDtoToTrack(
|
||||
api!,
|
||||
item,
|
||||
downloadedTracks ?? [],
|
||||
deviceProfile!,
|
||||
QueuingType.DirectlyQueued,
|
||||
),
|
||||
mapDtoToTrack(api!, item, deviceProfile!, QueuingType.DirectlyQueued),
|
||||
)
|
||||
|
||||
// Then update RNTP
|
||||
|
||||
@@ -153,41 +153,41 @@ const useSeekBy = () => {
|
||||
export const useAddToQueue = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: AddToQueueMutation) =>
|
||||
variables.queuingType === QueuingType.PlayingNext
|
||||
? playNextInQueue({ ...variables, downloadedTracks: getAudioCache() })
|
||||
: playLaterInQueue({ ...variables, downloadedTracks: getAudioCache() }),
|
||||
onSuccess: (_: void, { queuingType }: AddToQueueMutation) => {
|
||||
return useCallback(async (variables: AddToQueueMutation) => {
|
||||
try {
|
||||
if (variables.queuingType === QueuingType.PlayingNext) playNextInQueue({ ...variables })
|
||||
else playLaterInQueue({ ...variables })
|
||||
|
||||
trigger('notificationSuccess')
|
||||
console.debug(
|
||||
`${queuingType === QueuingType.PlayingNext ? 'Played next' : 'Added to queue'}`,
|
||||
`${variables.queuingType === QueuingType.PlayingNext ? 'Played next' : 'Added to queue'}`,
|
||||
)
|
||||
Toast.show({
|
||||
text1: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
|
||||
text1:
|
||||
variables.queuingType === QueuingType.PlayingNext
|
||||
? 'Playing next'
|
||||
: 'Added to queue',
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
onError: async (error: Error, { queuingType }: AddToQueueMutation) => {
|
||||
} catch (error) {
|
||||
trigger('notificationError')
|
||||
console.error(
|
||||
`Failed to ${queuingType === QueuingType.PlayingNext ? 'play next' : 'add to queue'}`,
|
||||
`Failed to ${variables.queuingType === QueuingType.PlayingNext ? 'play next' : 'add to queue'}`,
|
||||
error,
|
||||
)
|
||||
Toast.show({
|
||||
text1:
|
||||
queuingType === QueuingType.PlayingNext
|
||||
variables.queuingType === QueuingType.PlayingNext
|
||||
? 'Failed to play next'
|
||||
: 'Failed to add to queue',
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
onSettled: async () => {
|
||||
} finally {
|
||||
const newQueue = await TrackPlayer.getQueue()
|
||||
|
||||
usePlayerQueueStore.getState().setQueue(newQueue as JellifyTrack[])
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
export const useLoadNewQueue = () => {
|
||||
|
||||
@@ -4,6 +4,17 @@ import { BaseItemDto, MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client
|
||||
|
||||
export type SourceType = 'stream' | 'download'
|
||||
|
||||
export type BaseItemDtoSlimified = Pick<
|
||||
BaseItemDto,
|
||||
| 'Id'
|
||||
| 'SortName'
|
||||
| 'AlbumId'
|
||||
| 'ArtistItems'
|
||||
| 'ImageBlurHashes'
|
||||
| 'NormalizationGain'
|
||||
| 'RunTimeTicks'
|
||||
>
|
||||
|
||||
interface JellifyTrack extends Track {
|
||||
title?: string | undefined
|
||||
album?: string | undefined
|
||||
@@ -17,7 +28,7 @@ interface JellifyTrack extends Track {
|
||||
isLiveStream?: boolean | undefined
|
||||
|
||||
sourceType: SourceType
|
||||
item: BaseItemDto
|
||||
item: BaseItemDtoSlimified
|
||||
sessionId: string | null | undefined
|
||||
mediaSourceInfo?: MediaSourceInfo
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { BaseItemDtoSlimified } from '../types/JellifyTrack'
|
||||
|
||||
export function getBlurhashFromDto(
|
||||
{ ImageBlurHashes }: BaseItemDto,
|
||||
{ ImageBlurHashes }: BaseItemDto | BaseItemDtoSlimified,
|
||||
type: ImageType = ImageType.Primary,
|
||||
) {
|
||||
if (!ImageBlurHashes || !ImageBlurHashes[type]) return ''
|
||||
|
||||
@@ -21,6 +21,7 @@ import { convertRunTimeTicksToSeconds } from './runtimeticks'
|
||||
import { DownloadQuality } from '../stores/settings/usage'
|
||||
import MediaInfoQueryKey from '../api/queries/media/keys'
|
||||
import StreamingQuality from '../enums/audio-quality'
|
||||
import { getAudioCache } from '../api/mutations/download/offlineModeUtils'
|
||||
|
||||
/**
|
||||
* Gets quality-specific parameters for transcoding
|
||||
@@ -77,10 +78,10 @@ type TrackMediaInfo = Pick<
|
||||
export function mapDtoToTrack(
|
||||
api: Api,
|
||||
item: BaseItemDto,
|
||||
downloadedTracks: JellifyDownload[],
|
||||
deviceProfile: DeviceProfile,
|
||||
queuingType?: QueuingType,
|
||||
): JellifyTrack {
|
||||
const downloadedTracks = getAudioCache()
|
||||
const downloads = downloadedTracks.filter((download) => download.item.Id === item.Id)
|
||||
|
||||
const mediaInfo = queryClient.getQueryData(
|
||||
|
||||
Reference in New Issue
Block a user