import { useTypedSelector, type AppState, type Store } from '@/store'; import { Platform } from 'react-native'; import { version } from '../../../package.json'; import { Album, AlbumTrack, ArtistItem, Playlist } from '@/store/music/types'; type Credentials = AppState['settings']['credentials']; /** Map the output of `Platform.OS`, so that Jellyfin can understand it. */ const deviceMap: Record = { ios: 'iOS', android: 'Android', macos: 'macOS', web: 'Web', windows: 'Windows', }; /** * This is a convenience function that converts a set of Jellyfin credentials * from the Redux store to a HTTP Header that authenticates the user against the * Jellyfin server. */ function generateConfig(credentials: Credentials): RequestInit { return { headers: { 'X-Emby-Authorization': `MediaBrowser Client="Fintunes", Device="${deviceMap[Platform.OS]}", DeviceId="${credentials?.device_id}", Version="${version}", Token="${credentials?.access_token}"` } }; } /** * Retrieve a copy of the store without getting caught in import cycles. */ export function asyncFetchStore() { return require('@/store').default as Store; } export type PathOrCredentialInserter = string | ((credentials: NonNullable) => string); /** * A convenience function that accepts a request for fetch, injects it with the * proper Jellyfin credentials and attempts to catch any errors along the way. */ export async function fetchApi(path: PathOrCredentialInserter, providedConfig?: RequestInit, parseResponse?: true): Promise; export async function fetchApi(path: PathOrCredentialInserter, providedConfig: RequestInit, parseResponse: false): Promise; export async function fetchApi( path: PathOrCredentialInserter, providedConfig?: RequestInit, parseResponse = true ) { // Retrieve the latest credentials from the Redux store const credentials = asyncFetchStore().getState().settings.credentials; // GUARD: Check that the credentials are present if (!credentials) { throw new Error('Missing Jellyfin credentials when attempting API request'); } // Create the URL from the path and the credentials const resolvedPath = typeof path === 'function' ? path(credentials) : path; const url = `${credentials.uri}${resolvedPath.startsWith('/') ? '' : '/'}${resolvedPath}`; // Create config const config = { ...providedConfig, headers: { ...providedConfig?.headers, ...generateConfig(credentials).headers, } }; // Actually perform the request const response = await fetch(url, config); if (__DEV__) { console.log(`%c[HTTP] → [${response.status}] ${url}`, 'font-weight:bold;'); console.log('\t', config); } // GUARD: Check if the response is as expected if (!response.ok) { if (response.status === 403 || response.status === 401) { throw new Error('AuthenticationFailed'); } else if (response.status === 404) { throw new Error('ResourceNotFound'); } // Attempt to parse the error message try { const data = await response.json(); throw data; } catch { throw new Error('FailedRequest'); } } if (parseResponse) { // Parse body as JSON const data = await response.json() as Promise; return data; } return null; } /** * Retrieve an image URL for a given ItemId */ export function getImage(ItemId: string | number, credentials?: AppState['settings']['credentials']): string { // Either accept provided credentials, or retrieve them directly from the store const { uri: serverUri } = credentials ?? asyncFetchStore().getState().settings.credentials ?? {}; // Generate the uri and return const uri = encodeURI(`${serverUri}/Items/${ItemId}/Images/Primary?format=jpeg`); return uri; } /** * Create a hook that can convert ItemIds to image URLs */ export function useGetImage() { const credentials = useTypedSelector((state) => state.settings.credentials); return (item: string | number | Album | AlbumTrack | Playlist | ArtistItem | null) => { if (!item) { return ''; // GUARD: If the item's just the id, we'll pass it on directly. } else if (typeof item === 'string' || typeof item === 'number') { return getImage(item, credentials); // GUARD: If the item has an `PrimaryImageItemId` (for Emby servers), // we'll attemp to return that } else if ('PrimaryImageItemId' in item) { return getImage(item.PrimaryImageItemId || item.Id, credentials); } else { return getImage(item.Id); } }; }