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,
preferredExtension?: string | null,
): 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 {
const urlExtension = normalizeExtension(getExtensionFromUrl(url))
const hintedExtension = normalizeExtension(preferredExtension)
@@ -168,6 +173,12 @@ export const saveAudio = async (
//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 {
const downloadedTrackFile = await downloadJellyfinFile(
track.url,
@@ -177,10 +188,11 @@ export const saveAudio = async (
track.mediaSourceInfo?.Container,
)
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(
track.artwork as string,
track.item.Id as string,
track.artwork,
`${track.item.Id}-artwork`,
track.title as string,
setDownloadProgress,
undefined,

View File

@@ -23,6 +23,18 @@ import StreamingQuality from '../enums/audio-quality'
import { getAudioCache } from '../api/mutations/download/offlineModeUtils'
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.
* 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 {
// 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 {
type: TrackType.Default,
url: `file://${RNFS.DocumentDirectoryPath}/${downloadedTrack.path!.split('/').pop()}`,
image: `file://${RNFS.DocumentDirectoryPath}/${downloadedTrack.artwork!.split('/').pop()}`,
image: imagePath,
duration: convertRunTimeTicksToSeconds(
downloadedTrack.mediaSourceInfo?.RunTimeTicks || downloadedTrack.item.RunTimeTicks || 0,
),
item: downloadedTrack.item,
mediaSourceInfo: downloadedTrack.mediaSourceInfo,
sessionId: downloadedTrack.sessionId,
sessionId: getValidSessionId(downloadedTrack.sessionId),
sourceType: 'download',
}
}
@@ -198,7 +215,7 @@ function buildTranscodedTrack(
duration: convertRunTimeTicksToSeconds(RunTimeTicks ?? 0),
mediaSourceInfo,
item,
sessionId,
sessionId: getValidSessionId(sessionId),
sourceType: 'stream',
}
}
@@ -228,7 +245,7 @@ function buildAudioApiUrl(
const mediaSource = mediaInfo!.MediaSources![0]
urlParams = {
playSessionId: mediaInfo?.PlaySessionId ?? uuid.v4(),
playSessionId: getValidSessionId(mediaInfo?.PlaySessionId),
startTimeTicks: '0',
static: 'true',
}