feat: Add URL validation before file downloads and ensure valid session IDs for tracks and streams. (#813)

This commit is contained in:
skalthoff
2025-12-16 02:54:40 -08:00
committed by GitHub
parent b9deee7623
commit 621f7c38fb
2 changed files with 36 additions and 7 deletions

View File

@@ -60,6 +60,11 @@ export async function downloadJellyfinFile(
setDownloadProgress: JellifyDownloadProgressState, setDownloadProgress: JellifyDownloadProgressState,
preferredExtension?: string | null, preferredExtension?: string | null,
): Promise<DownloadedFileInfo> { ): Promise<DownloadedFileInfo> {
// Validate URL before attempting download to prevent NPE in native code
if (!url || url.trim() === '') {
throw new Error('Invalid download URL: URL is empty or undefined')
}
try { try {
const urlExtension = normalizeExtension(getExtensionFromUrl(url)) const urlExtension = normalizeExtension(getExtensionFromUrl(url))
const hintedExtension = normalizeExtension(preferredExtension) const hintedExtension = normalizeExtension(preferredExtension)
@@ -168,6 +173,12 @@ export const saveAudio = async (
//Ignore //Ignore
} }
// Validate track URL before attempting download
if (!track.url || track.url.trim() === '') {
console.error('Cannot download track: URL is missing', track.item.Id)
return false
}
try { try {
const downloadedTrackFile = await downloadJellyfinFile( const downloadedTrackFile = await downloadJellyfinFile(
track.url, track.url,
@@ -177,10 +188,11 @@ export const saveAudio = async (
track.mediaSourceInfo?.Container, track.mediaSourceInfo?.Container,
) )
let downloadedArtworkFile: DownloadedFileInfo | undefined let downloadedArtworkFile: DownloadedFileInfo | undefined
if (track.artwork) { // Check for non-empty artwork URL (empty string passes truthy check but fails download)
if (track.artwork && typeof track.artwork === 'string' && track.artwork.trim() !== '') {
downloadedArtworkFile = await downloadJellyfinFile( downloadedArtworkFile = await downloadJellyfinFile(
track.artwork as string, track.artwork,
track.item.Id as string, `${track.item.Id}-artwork`,
track.title as string, track.title as string,
setDownloadProgress, setDownloadProgress,
undefined, undefined,

View File

@@ -23,6 +23,18 @@ import StreamingQuality from '../enums/audio-quality'
import { getAudioCache } from '../api/mutations/download/offlineModeUtils' import { getAudioCache } from '../api/mutations/download/offlineModeUtils'
import RNFS from 'react-native-fs' import RNFS from 'react-native-fs'
/**
* Ensures a valid session ID is returned.
* The ?? operator doesn't catch empty strings, so we need this helper.
* Empty session IDs cause MusicService to crash with "Session ID must be unique. ID="
*/
function getValidSessionId(sessionId: string | null | undefined): string {
if (sessionId && sessionId.trim() !== '') {
return sessionId
}
return uuid.v4().toString()
}
/** /**
* Gets the artwork URL for a track, prioritizing the track's own artwork over the album's artwork. * Gets the artwork URL for a track, prioritizing the track's own artwork over the album's artwork.
* Falls back to artist image if no album artwork is available. * Falls back to artist image if no album artwork is available.
@@ -169,16 +181,21 @@ function ensureFileUri(path?: string): string | undefined {
} }
function buildDownloadedTrack(downloadedTrack: JellifyDownload): TrackMediaInfo { function buildDownloadedTrack(downloadedTrack: JellifyDownload): TrackMediaInfo {
// Safely build the image path - artwork is optional and may be undefined
const imagePath = downloadedTrack.artwork
? `file://${RNFS.DocumentDirectoryPath}/${downloadedTrack.artwork.split('/').pop()}`
: undefined
return { return {
type: TrackType.Default, type: TrackType.Default,
url: `file://${RNFS.DocumentDirectoryPath}/${downloadedTrack.path!.split('/').pop()}`, url: `file://${RNFS.DocumentDirectoryPath}/${downloadedTrack.path!.split('/').pop()}`,
image: `file://${RNFS.DocumentDirectoryPath}/${downloadedTrack.artwork!.split('/').pop()}`, image: imagePath,
duration: convertRunTimeTicksToSeconds( duration: convertRunTimeTicksToSeconds(
downloadedTrack.mediaSourceInfo?.RunTimeTicks || downloadedTrack.item.RunTimeTicks || 0, downloadedTrack.mediaSourceInfo?.RunTimeTicks || downloadedTrack.item.RunTimeTicks || 0,
), ),
item: downloadedTrack.item, item: downloadedTrack.item,
mediaSourceInfo: downloadedTrack.mediaSourceInfo, mediaSourceInfo: downloadedTrack.mediaSourceInfo,
sessionId: downloadedTrack.sessionId, sessionId: getValidSessionId(downloadedTrack.sessionId),
sourceType: 'download', sourceType: 'download',
} }
} }
@@ -198,7 +215,7 @@ function buildTranscodedTrack(
duration: convertRunTimeTicksToSeconds(RunTimeTicks ?? 0), duration: convertRunTimeTicksToSeconds(RunTimeTicks ?? 0),
mediaSourceInfo, mediaSourceInfo,
item, item,
sessionId, sessionId: getValidSessionId(sessionId),
sourceType: 'stream', sourceType: 'stream',
} }
} }
@@ -228,7 +245,7 @@ function buildAudioApiUrl(
const mediaSource = mediaInfo!.MediaSources![0] const mediaSource = mediaInfo!.MediaSources![0]
urlParams = { urlParams = {
playSessionId: mediaInfo?.PlaySessionId ?? uuid.v4(), playSessionId: getValidSessionId(mediaInfo?.PlaySessionId),
startTimeTicks: '0', startTimeTicks: '0',
static: 'true', static: 'true',
} }