diff --git a/src/apps/experimental/components/library/ItemsView.tsx b/src/apps/experimental/components/library/ItemsView.tsx index 93be9d1348..de9d0c6d6f 100644 --- a/src/apps/experimental/components/library/ItemsView.tsx +++ b/src/apps/experimental/components/library/ItemsView.tsx @@ -6,7 +6,7 @@ import React, { type FC, useCallback } from 'react'; import Box from '@mui/material/Box'; import classNames from 'classnames'; import { useLocalStorage } from 'hooks/useLocalStorage'; -import { useGetItem, useGetItemsViewByType } from 'hooks/useFetchItems'; +import { useGetItemsViewByType } from 'hooks/useFetchItems'; import { getDefaultLibraryViewSettings, getSettingsKey } from 'utils/items'; import { CardShape } from 'utils/card'; import Loading from 'components/loading/LoadingComponent'; @@ -28,6 +28,7 @@ import { LibraryTab } from 'types/libraryTab'; import { type LibraryViewSettings, type ParentId, ViewMode } from 'types/library'; import type { CardOptions } from 'types/cardOptions'; import type { ListOptions } from 'types/listOptions'; +import { useItem } from 'hooks/useItem'; interface ItemsViewProps { viewType: LibraryTab; @@ -79,7 +80,7 @@ const ItemsView: FC = ({ itemType, libraryViewSettings ); - const { data: item } = useGetItem(parentId); + const { data: item } = useItem(parentId || undefined); const getListOptions = useCallback(() => { const listOptions: ListOptions = { diff --git a/src/components/playback/displayMirrorManager.ts b/src/components/playback/displayMirrorManager.ts index f74b3e10fb..a8a7b1b7a1 100644 --- a/src/components/playback/displayMirrorManager.ts +++ b/src/components/playback/displayMirrorManager.ts @@ -1,38 +1,40 @@ -import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import ServerConnections from 'components/ServerConnections'; +import { getItemQuery } from 'hooks/useItem'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { queryClient } from 'utils/query/queryClient'; import { playbackManager } from './playbackmanager'; -interface PlaybackInfo { - item: BaseItemDto; - context?: string; -} - -function mirrorItem(info: PlaybackInfo, player?: unknown) { - const { item } = info; - - playbackManager.displayContent({ - ItemName: item.Name, - ItemId: item.Id, - ItemType: item.Type, - Context: info.context - }, player); -} - -function mirrorIfEnabled(info: PlaybackInfo) { - if (info && playbackManager.enableDisplayMirroring()) { +async function mirrorIfEnabled(serverId: string, itemId: string) { + if (playbackManager.enableDisplayMirroring()) { const playerInfo = playbackManager.getPlayerInfo(); if (playerInfo && !playerInfo.isLocalPlayer && playerInfo.supportedCommands.indexOf('DisplayContent') !== -1) { - mirrorItem(info, playbackManager.getCurrentPlayer()); + const apiClient = ServerConnections.getApiClient(serverId); + const api = toApi(apiClient); + const userId = apiClient.getCurrentUserId(); + + try { + const item = await queryClient.fetchQuery(getItemQuery( + api, + userId, + itemId)); + + playbackManager.displayContent({ + ItemName: item.Name, + ItemId: item.Id, + ItemType: item.Type + }, playbackManager.getCurrentPlayer()); + } catch (err) { + console.error('[DisplayMirrorManager] failed to mirror item', err); + } } } } document.addEventListener('viewshow', e => { - const state = e.detail.state || {}; - const { item } = state; - - if (item?.ServerId) { - mirrorIfEnabled({ item }); + const { serverId, id } = e.detail?.params || {}; + if (serverId && id) { + void mirrorIfEnabled(serverId, id); } }); diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index de6b3c6871..3b07a5571c 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -1,3 +1,4 @@ +import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; import { Action, createHashHistory } from 'history'; import { appHost } from '../apphost'; @@ -9,8 +10,11 @@ import loading from '../loading/loading'; import viewManager from '../viewManager/viewManager'; import ServerConnections from '../ServerConnections'; import alert from '../alert'; -import { ConnectionState } from '../../utils/jellyfin-apiclient/ConnectionState.ts'; -import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; + +import { queryClient } from 'utils/query/queryClient'; +import { getItemQuery } from 'hooks/useItem'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState.ts'; export const history = createHashHistory(); @@ -183,18 +187,26 @@ class AppRouter { showItem(item, serverId, options) { // TODO: Refactor this so it only gets items, not strings. - if (typeof (item) === 'string') { + if (typeof item === 'string') { const apiClient = serverId ? ServerConnections.getApiClient(serverId) : ServerConnections.currentApiClient(); - apiClient.getItem(apiClient.getCurrentUserId(), item).then((itemObject) => { - this.showItem(itemObject, options); - }); + const api = toApi(apiClient); + const userId = apiClient.getCurrentUserId(); + + queryClient + .fetchQuery(getItemQuery(api, userId, item)) + .then(itemObject => { + this.showItem(itemObject, options); + }) + .catch(err => { + console.error('[AppRouter] Failed to fetch item', err); + }); } else { if (arguments.length === 2) { options = arguments[1]; } const url = this.getRouteUrl(item, options); - this.show(url, { item }); + this.show(url); } } diff --git a/src/components/themeMediaPlayer.js b/src/components/themeMediaPlayer.js index bad76612ce..87d86b7a4b 100644 --- a/src/components/themeMediaPlayer.js +++ b/src/components/themeMediaPlayer.js @@ -1,6 +1,14 @@ +import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type'; +import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'; + +import { getItemQuery } from 'hooks/useItem'; +import { currentSettings as userSettings } from 'scripts/settings/userSettings'; +import { ItemKind } from 'types/base/models/item-kind'; +import Events from 'utils/events.ts'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; +import { queryClient } from 'utils/query/queryClient'; + import { playbackManager } from './playback/playbackmanager'; -import * as userSettings from '../scripts/settings/userSettings'; -import Events from '../utils/events.ts'; import ServerConnections from './ServerConnections'; let currentOwnerId; @@ -50,44 +58,61 @@ function stopIfPlaying() { } function enabled(mediaType) { - if (mediaType === 'Video') { + if (mediaType === MediaType.Video) { return userSettings.enableThemeVideos(); } return userSettings.enableThemeSongs(); } -const excludeTypes = ['CollectionFolder', 'UserView', 'Program', 'SeriesTimer', 'Person', 'TvChannel', 'Channel']; +const excludeTypes = [ + ItemKind.CollectionFolder, + ItemKind.UserView, + ItemKind.Person, + ItemKind.Program, + ItemKind.TvChannel, + ItemKind.Channel, + ItemKind.SeriesTimer +]; -function loadThemeMedia(item) { - if (item.CollectionType) { - stopIfPlaying(); - return; - } +async function loadThemeMedia(serverId, itemId) { + const apiClient = ServerConnections.getApiClient(serverId); + const api = toApi(apiClient); + const userId = apiClient.getCurrentUserId(); - if (excludeTypes.indexOf(item.Type) !== -1) { - stopIfPlaying(); - return; - } + try { + const item = await queryClient.fetchQuery(getItemQuery( + api, + userId, + itemId)); - const apiClient = ServerConnections.getApiClient(item.ServerId); - apiClient.getThemeMedia(apiClient.getCurrentUserId(), item.Id, true).then(function (themeMediaResult) { - const result = userSettings.enableThemeVideos() && themeMediaResult.ThemeVideosResult.Items.length ? themeMediaResult.ThemeVideosResult : themeMediaResult.ThemeSongsResult; - - const ownerId = result.OwnerId; - - if (ownerId !== currentOwnerId) { - playThemeMedia(result.Items, ownerId); + if (item.CollectionType) { + stopIfPlaying(); + return; } - }); + + if (excludeTypes.includes(item.Type)) { + stopIfPlaying(); + return; + } + + const { data: themeMedia } = await getLibraryApi(api) + .getThemeMedia({ userId, itemId: item.Id, inheritFromParent: true }); + + const result = userSettings.enableThemeVideos() && themeMedia.ThemeVideosResult?.Items?.length ? themeMedia.ThemeVideosResult : themeMedia.ThemeSongsResult; + + if (result.OwnerId !== currentOwnerId) { + playThemeMedia(result.Items, result.OwnerId); + } + } catch (err) { + console.error('[ThemeMediaPlayer] failed to load theme media', err); + } } -document.addEventListener('viewshow', function (e) { - const state = e.detail.state || {}; - const item = state.item; - - if (item?.ServerId) { - loadThemeMedia(item); +document.addEventListener('viewshow', e => { + const { serverId, id } = e.detail?.params || {}; + if (serverId && id) { + void loadThemeMedia(serverId, id); return; } @@ -100,7 +125,7 @@ document.addEventListener('viewshow', function (e) { } }, true); -Events.on(playbackManager, 'playbackstart', function (e, player) { +Events.on(playbackManager, 'playbackstart', (_e, player) => { const item = playbackManager.currentItem(player); // User played something manually if (currentThemeIds.indexOf(item.Id) == -1) { diff --git a/src/hooks/useFetchItems.ts b/src/hooks/useFetchItems.ts index 2d4f23f1a9..710b8863a6 100644 --- a/src/hooks/useFetchItems.ts +++ b/src/hooks/useFetchItems.ts @@ -28,36 +28,6 @@ import { LibraryViewSettings, ParentId } from 'types/library'; import { LibraryTab } from 'types/libraryTab'; import { Section, SectionApiMethod, SectionType } from 'types/sections'; -const fetchGetItem = async ( - currentApi: JellyfinApiContext, - parentId: ParentId, - options?: AxiosRequestConfig -) => { - const { api, user } = currentApi; - if (api && user?.Id && parentId) { - const response = await getUserLibraryApi(api).getItem( - { - userId: user.Id, - itemId: parentId - }, - { - signal: options?.signal - } - ); - return response.data; - } -}; - -export const useGetItem = (parentId: ParentId) => { - const currentApi = useApi(); - const isLivetv = parentId === 'livetv'; - return useQuery({ - queryKey: ['Item', parentId], - queryFn: ({ signal }) => fetchGetItem(currentApi, parentId, { signal }), - enabled: !!parentId && !isLivetv - }); -}; - const fetchGetItems = async ( currentApi: JellyfinApiContext, parametersOptions: ItemsApiGetItemsRequest, diff --git a/src/hooks/useItem.ts b/src/hooks/useItem.ts new file mode 100644 index 0000000000..ab72e1928f --- /dev/null +++ b/src/hooks/useItem.ts @@ -0,0 +1,41 @@ +import type { Api } from '@jellyfin/sdk/lib/api'; +import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'; +import { useQuery } from '@tanstack/react-query'; +import type { AxiosRequestConfig } from 'axios'; + +import { queryOptions } from 'utils/query/queryOptions'; + +import { useApi } from './useApi'; + +const fetchItem = async ( + api?: Api, + userId?: string, + itemId?: string, + options?: AxiosRequestConfig +) => { + if (!api) throw new Error('No API instance available'); + if (!itemId) throw new Error('No item ID provided'); + + const response = await getUserLibraryApi(api) + .getItem({ userId, itemId }, options); + return response.data; +}; + +export const getItemQuery = ( + api?: Api, + userId?: string, + itemId?: string +) => queryOptions({ + queryKey: [ 'User', userId, 'Items', itemId ], + queryFn: ({ signal }) => fetchItem(api, userId, itemId, { signal }), + staleTime: 1000, // 1 second + enabled: !!api && !!userId && !!itemId +}); + +export const useItem = ( + itemId?: string +) => { + const apiContext = useApi(); + const { api, user } = apiContext; + return useQuery(getItemQuery(api, user?.Id, itemId)); +};