Merge pull request #4116 from thornbill/add-api-context

Add api and user context
This commit is contained in:
Bill Thornton 2022-12-15 11:49:22 -05:00 committed by GitHub
commit 90c08d856c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 142 additions and 27 deletions

18
src/App.tsx Normal file
View file

@ -0,0 +1,18 @@
import { History } from '@remix-run/router';
import React from 'react';
import { HistoryRouter } from './components/HistoryRouter';
import { ApiProvider } from './hooks/useApi';
import AppRoutes from './routes/index';
const App = ({ history }: { history: History }) => {
return (
<ApiProvider>
<HistoryRouter history={history}>
<AppRoutes />
</HistoryRouter>
</ApiProvider>
);
};
export default App;

1
src/apiclient.d.ts vendored
View file

@ -117,6 +117,7 @@ declare module 'jellyfin-apiclient' {
getCountries(): Promise<CountryInfo[]>;
getCriticReviews(itemId: string, options?: any): Promise<BaseItemDtoQueryResult>;
getCultures(): Promise<CultureDto[]>;
getCurrentUser(cache?: boolean): Promise<UserDto>;
getCurrentUserId(): string;
getDateParamValue(date: Date): string;
getDefaultImageQuality(imageType: ImageType): number;

View file

@ -128,8 +128,8 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
// If this is an admin route, ensure the user has access
if (isAdminRequired) {
try {
const user = await client.getCurrentUser();
if (!user.Policy.IsAdministrator) {
const user = await client?.getCurrentUser();
if (!user?.Policy?.IsAdministrator) {
console.warn('[ConnectionRequired] normal user attempted to access admin route');
bounce(await ServerConnections.connect());
return;

View file

@ -86,6 +86,10 @@ class ServerConnections extends ConnectionManager {
return this.localApiClient;
}
/**
* Gets the ApiClient that is currently connected.
* @returns {ApiClient|undefined} apiClient
*/
currentApiClient() {
let apiClient = this.getLocalApiClient();

View file

@ -34,7 +34,7 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
const apiClient = ServerConnections.currentApiClient();
// Fetch the view html from the server and translate it
const viewHtml = await apiClient.get(apiClient.getUrl(view + location.search))
const viewHtml = await apiClient?.get(apiClient.getUrl(view + location.search))
.then((html: string) => globalize.translateHtml(html));
viewManager.loadView({

View file

@ -1,10 +1,13 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
import escapeHtml from 'escape-html';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { appRouter } from '../appRouter';
import { useApi } from '../../hooks/useApi';
import globalize from '../../scripts/globalize';
import ServerConnections from '../ServerConnections';
import '../../elements/emby-button/emby-button';
@ -21,27 +24,30 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) =>
});
type SearchSuggestionsProps = {
serverId?: string;
parentId?: string | null;
}
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => {
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }: SearchSuggestionsProps) => {
const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]);
const { api, user } = useApi();
useEffect(() => {
const apiClient = ServerConnections.getApiClient(serverId);
apiClient.getItems(apiClient.getCurrentUserId(), {
SortBy: 'IsFavoriteOrLiked,Random',
IncludeItemTypes: 'Movie,Series,MusicArtist',
Limit: 20,
Recursive: true,
ImageTypeLimit: 0,
EnableImages: false,
ParentId: parentId,
EnableTotalRecordCount: false
}).then(result => setSuggestions(result.Items || []));
}, [parentId, serverId]);
if (api && user?.Id) {
getItemsApi(api)
.getItemsByUserId({
userId: user.Id,
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
includeItemTypes: [BaseItemKind.Movie, BaseItemKind.Series, BaseItemKind.MusicArtist],
limit: 20,
recursive: true,
imageTypeLimit: 0,
enableImages: false,
parentId: parentId || undefined,
enableTotalRecordCount: false
})
.then(result => setSuggestions(result.data.Items || []));
}
}, [ api, parentId, user ]);
return (
<div

67
src/hooks/useApi.tsx Normal file
View file

@ -0,0 +1,67 @@
import type { Api } from '@jellyfin/sdk';
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import type { ApiClient, Event } from 'jellyfin-apiclient';
import React, { createContext, FC, useContext, useEffect, useState } from 'react';
import ServerConnections from '../components/ServerConnections';
import events from '../utils/events';
import { toApi } from '../utils/jellyfin-apiclient/compat';
interface JellyfinApiContext {
__legacyApiClient__?: ApiClient
api?: Api
user?: UserDto
}
export const ApiContext = createContext<JellyfinApiContext>({});
export const useApi = () => useContext(ApiContext);
export const ApiProvider: FC = ({ children }) => {
const [ legacyApiClient, setLegacyApiClient ] = useState<ApiClient>();
const [ api, setApi ] = useState<Api>();
const [ user, setUser ] = useState<UserDto>();
useEffect(() => {
ServerConnections.currentApiClient()
?.getCurrentUser()
.then(newUser => updateApiUser(undefined, newUser))
.catch(err => {
console.info('[ApiProvider] Could not get current user', err);
});
const updateApiUser = (_e: Event | undefined, newUser: UserDto) => {
setUser(newUser);
if (newUser.ServerId) {
setLegacyApiClient(ServerConnections.getApiClient(newUser.ServerId));
}
};
const resetApiUser = () => {
setLegacyApiClient(undefined);
setUser(undefined);
};
events.on(ServerConnections, 'localusersignedin', updateApiUser);
events.on(ServerConnections, 'localusersignedout', resetApiUser);
return () => {
events.off(ServerConnections, 'localusersignedin', updateApiUser);
events.off(ServerConnections, 'localusersignedout', resetApiUser);
};
}, [ setLegacyApiClient, setUser ]);
useEffect(() => {
setApi(legacyApiClient ? toApi(legacyApiClient) : undefined);
}, [ legacyApiClient, setApi ]);
return (
<ApiContext.Provider value={{
__legacyApiClient__: legacyApiClient,
api,
user
}}>
{children}
</ApiContext.Provider>
);
};

View file

@ -37,8 +37,7 @@ import './legacy/htmlMediaElement';
import './legacy/vendorStyles';
import { currentSettings } from './scripts/settings/userSettings';
import taskButton from './scripts/taskbutton';
import { HistoryRouter } from './components/HistoryRouter.tsx';
import AppRoutes from './routes/index.tsx';
import App from './App.tsx';
function loadCoreDictionary() {
const languages = ['af', 'ar', 'be-by', 'bg-bg', 'bn_bd', 'ca', 'cs', 'cy', 'da', 'de', 'el', 'en-gb', 'en-us', 'eo', 'es', 'es-419', 'es-ar', 'es_do', 'es-mx', 'et', 'eu', 'fa', 'fi', 'fil', 'fr', 'fr-ca', 'gl', 'gsw', 'he', 'hi-in', 'hr', 'hu', 'id', 'it', 'ja', 'kk', 'ko', 'lt-lt', 'lv', 'mr', 'ms', 'nb', 'nl', 'nn', 'pl', 'pr', 'pt', 'pt-br', 'pt-pt', 'ro', 'ru', 'sk', 'sl-si', 'sq', 'sv', 'ta', 'th', 'tr', 'uk', 'ur_pk', 'vi', 'zh-cn', 'zh-hk', 'zh-tw'];
@ -146,9 +145,7 @@ async function onAppReady() {
ReactDOM.render(
<StrictMode>
<HistoryRouter history={history}>
<AppRoutes />
</HistoryRouter>
<App history={history} />
</StrictMode>,
document.getElementById('reactRoot')
);

View file

@ -71,7 +71,7 @@ class Manager {
/**
* Update active ApiClient.
* @param {Object} apiClient The ApiClient.
* @param {ApiClient|undefined} apiClient The ApiClient.
*/
updateApiClient(apiClient) {
if (!apiClient) {

View file

@ -21,7 +21,6 @@ const Search: FunctionComponent = () => {
<SearchFields onSearch={setQuery} />
{!query &&
<SearchSuggestions
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
parentId={searchParams.get('parentId')}
/>
}

View file

@ -0,0 +1,23 @@
import { Api, Jellyfin } from '@jellyfin/sdk';
import { ApiClient } from 'jellyfin-apiclient';
/**
* Returns an SDK Api instance using the same parameters as the provided ApiClient.
* @param {ApiClient} apiClient The (legacy) ApiClient.
* @returns {Api} An equivalent SDK Api instance.
*/
export const toApi = (apiClient: ApiClient): Api => {
return (new Jellyfin({
clientInfo: {
name: apiClient.appName(),
version: apiClient.appVersion()
},
deviceInfo: {
name: apiClient.deviceName(),
id: apiClient.deviceId()
}
})).createApi(
apiClient.serverAddress(),
apiClient.accessToken()
);
};

View file

@ -146,7 +146,7 @@ const config = {
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|@remix-run[\\/]router|blurhash|date-fns|dom7|epubjs|flv.js|libarchive.js|marked|react-router|screenfull|ssr-window|swiper)/,
exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|@remix-run[\\/]router|blurhash|compare-versions|date-fns|dom7|epubjs|flv.js|libarchive.js|marked|react-router|screenfull|ssr-window|swiper)/,
use: [{
loader: 'babel-loader',
options: {