Artwork Retrieval Fixes and Network Timeout Improvements (#760)

* fix: increase default timeout for nitroFetch and Axios instance to 60 seconds

* feat: prioritize track artwork over album artwork in image URL retrieval

* fix: enhance image retrieval logic to prioritize album artist's image if album artwork is unavailable
This commit is contained in:
skalthoff
2025-12-05 13:11:45 -08:00
committed by GitHub
parent 9e9af8be72
commit 9aa13acda3
5 changed files with 95 additions and 23 deletions

View File

@@ -20,7 +20,7 @@ export function getItemImageUrl(
type: ImageType,
options?: ImageUrlOptions,
): string | undefined {
const { AlbumId, AlbumPrimaryImageTag, ImageTags, Id } = item
const { AlbumId, AlbumPrimaryImageTag, ImageTags, Id, AlbumArtists } = item
if (!api) return undefined
@@ -32,15 +32,33 @@ export function getItemImageUrl(
quality: options?.quality ?? 90,
}
return AlbumId
? getImageApi(api).getItemImageUrlById(AlbumId, type, {
...imageParams,
tag: AlbumPrimaryImageTag ?? undefined,
})
: Id
? getImageApi(api).getItemImageUrlById(Id, type, {
...imageParams,
tag: ImageTags ? ImageTags[type] : undefined,
})
: undefined
// Check if the item has its own image for the requested type first
const hasOwnImage = ImageTags && ImageTags[type]
if (hasOwnImage && Id) {
// Use the item's own image (e.g., track-specific artwork)
return getImageApi(api).getItemImageUrlById(Id, type, {
...imageParams,
tag: ImageTags[type],
})
} else if (AlbumId && AlbumPrimaryImageTag) {
// Fall back to album image (only if the album has an image)
return getImageApi(api).getItemImageUrlById(AlbumId, type, {
...imageParams,
tag: AlbumPrimaryImageTag,
})
} else if (AlbumArtists && AlbumArtists.length > 0 && AlbumArtists[0].Id) {
// Fall back to first album artist's image
return getImageApi(api).getItemImageUrlById(AlbumArtists[0].Id, type, {
...imageParams,
})
} else if (Id) {
// Last resort: use item's own ID
return getImageApi(api).getItemImageUrlById(Id, type, {
...imageParams,
tag: ImageTags ? ImageTags[type] : undefined,
})
}
return undefined
}

View File

@@ -13,7 +13,7 @@ export async function nitroFetch<T>(
api: Api | undefined,
path: string,
params?: Record<string, string | number | boolean | undefined | string[]>,
timeoutMs: number = 30000,
timeoutMs: number = 60000,
): Promise<T> {
if (isUndefined(api)) {
throw new Error('Client instance not set')

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react'
import Input from '../Global/helpers/input'
import { Text } from '../Global/helpers/text'
import ItemRow from '../Global/components/item-row'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { QueryKeys } from '../../enums/query-keys'
@@ -108,9 +109,37 @@ export default function Search({
}
ItemSeparatorComponent={() => <Separator />}
ListEmptyComponent={() => {
// Show spinner while fetching
if (fetchingResults) {
return (
<YStack alignContent='center' justifyContent='center' marginTop={'$4'}>
<Spinner />
</YStack>
)
}
// Show "No Results" when user has searched but got no results
if (!isEmpty(searchString) && isEmpty(items)) {
return (
<YStack
alignItems='center'
justifyContent='center'
marginTop={'$8'}
gap={'$3'}
paddingHorizontal={'$4'}
>
<H3>No Results</H3>
<Text textAlign='center'>
{`No results found for "${searchString}". Try a different search term.`}
</Text>
</YStack>
)
}
// Show suggestions when no search is active
return (
<YStack alignContent='center' justifyContent='flex-end' marginTop={'$4'}>
{fetchingResults ? <Spinner /> : <Suggestions suggestions={suggestions} />}
<Suggestions suggestions={suggestions} />
</YStack>
)
}}

View File

@@ -3,10 +3,10 @@ import axios from 'axios'
/**
* The Axios instance for making HTTP requests.
*
* Default timeout is set to 15 seconds.
* Default timeout is set to 60 seconds.
*/
const AXIOS_INSTANCE = axios.create({
timeout: 15 * 1000, // 15 seconds
timeout: 60000,
})
export default AXIOS_INSTANCE

View File

@@ -23,6 +23,35 @@ import StreamingQuality from '../enums/audio-quality'
import { getAudioCache } from '../api/mutations/download/offlineModeUtils'
import RNFS from 'react-native-fs'
/**
* 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.
*
* @param api The API instance
* @param item The track item
* @returns The artwork URL or undefined
*/
function getTrackArtworkUrl(api: Api, item: BaseItemDto): string | undefined {
const { AlbumId, AlbumPrimaryImageTag, ImageTags, Id, AlbumArtists } = item
// Check if the track has its own Primary image
if (ImageTags?.Primary && Id) {
return getImageApi(api).getItemImageUrlById(Id, ImageType.Primary)
}
// Fall back to album artwork (only if the album has an image)
if (AlbumId && AlbumPrimaryImageTag) {
return getImageApi(api).getItemImageUrlById(AlbumId, ImageType.Primary)
}
// Fall back to first album artist's image
if (AlbumArtists && AlbumArtists.length > 0 && AlbumArtists[0].Id) {
return getImageApi(api).getItemImageUrlById(AlbumArtists[0].Id, ImageType.Primary)
}
return undefined
}
/**
* Gets quality-specific parameters for transcoding
*
@@ -108,9 +137,7 @@ export function mapDtoToTrack(
} else
trackMediaInfo = {
url: buildAudioApiUrl(api, item, deviceProfile),
image: item.AlbumId
? getImageApi(api).getItemImageUrlById(item.AlbumId, ImageType.Primary)
: undefined,
image: getTrackArtworkUrl(api, item),
duration: convertRunTimeTicksToSeconds(item.RunTimeTicks!),
item,
sessionId: mediaInfo?.PlaySessionId,
@@ -162,14 +189,12 @@ function buildTranscodedTrack(
mediaSourceInfo: MediaSourceInfo,
sessionId: string | null | undefined,
): TrackMediaInfo {
const { AlbumId, RunTimeTicks } = item
const { RunTimeTicks } = item
return {
type: TrackType.HLS,
url: `${api.basePath}${mediaSourceInfo.TranscodingUrl}`,
image: AlbumId
? getImageApi(api).getItemImageUrlById(AlbumId, ImageType.Primary)
: undefined,
image: getTrackArtworkUrl(api, item),
duration: convertRunTimeTicksToSeconds(RunTimeTicks ?? 0),
mediaSourceInfo,
item,