Add image URL generation at runtime to allow for dynamic image sizes (#1439)

* add getImageUrl to domain endpoints

* add new ItemImage component and hooks to generate image url

* add configuration for image resolution based on types
This commit is contained in:
Jeff
2025-12-23 20:18:52 -08:00
committed by GitHub
parent 96f38e597c
commit 25bfb65b6d
39 changed files with 823 additions and 670 deletions
+14
View File
@@ -370,6 +370,20 @@ export const controller: GeneralController = {
query: mergeMusicFolderId(args.query, server),
});
},
getImageUrl(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
return null;
}
return (
apiController(
'getImageUrl',
server.type,
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }) || null
);
},
getInternetRadioStations(args) {
const server = getServerById(args.apiClientProps.serverId);
@@ -670,6 +670,22 @@ export const JellyfinController: InternalControllerEndpoint = {
totalRecordCount: res.body?.TotalRecordCount || 0,
};
},
getImageUrl: ({ apiClientProps: { server }, query }) => {
const { id, size } = query;
const imageSize = size;
if (!server?.url) {
return null;
}
// For Jellyfin, we construct the URL pattern
// The server will return a 404 or placeholder if no image exists
const baseUrl = `${server.url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`;
// For songs, we might want to fall back to album art, but we don't have albumId here
// The caller can handle this if needed
return baseUrl;
},
getInternetRadioStations: async (args) => {
const { apiClientProps } = args;
@@ -1077,9 +1093,7 @@ export const JellyfinController: InternalControllerEndpoint = {
}
return {
items: items.map((item) =>
jfNormalize.song(item, apiClientProps.server, query.imageSize),
),
items: items.map((item) => jfNormalize.song(item, apiClientProps.server)),
startIndex: query.startIndex,
totalRecordCount,
};
@@ -461,6 +461,7 @@ export const NavidromeController: InternalControllerEndpoint = {
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
},
getImageUrl: SubsonicController.getImageUrl,
getInternetRadioStations: SubsonicController.getInternetRadioStations,
getLyrics: SubsonicController.getLyrics,
getMusicFolderList: SubsonicController.getMusicFolderList,
@@ -664,9 +665,7 @@ export const NavidromeController: InternalControllerEndpoint = {
}
return {
items: res.body.data.map((song) =>
ndNormalize.song(song, apiClientProps.server, query.imageSize),
),
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
@@ -827,6 +827,28 @@ export const SubsonicController: InternalControllerEndpoint = {
startIndex: query.startIndex,
});
},
getImageUrl: ({ apiClientProps: { server }, query }) => {
const { id, size } = query;
const imageSize = size;
if (!server?.url || !server?.credential) {
return null;
}
// Check for default placeholder image ID
if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
return null;
}
return (
`${server.url}/rest/getCoverArt.view` +
`?id=${id}` +
`&${server.credential}` +
'&v=1.13.0' +
'&c=Feishin' +
(imageSize ? `&size=${imageSize}` : '')
);
},
getInternetRadioStations: async (args) => {
const { apiClientProps } = args;