From 678f946e28b18974cc1513c5d21a42b0310b72ee Mon Sep 17 00:00:00 2001 From: Violet Caulfield Date: Sun, 19 Jan 2025 09:52:26 -0600 Subject: [PATCH] Can I switch to a singleton for the api client? --- api/client.ts | 144 +++++++++++++++----- api/info.ts | 18 +++ api/queries/functions/api.ts | 10 +- components/Login/helpers/server-address.tsx | 4 +- components/jellyfin-api-provider.tsx | 26 ++-- index.js | 2 + 6 files changed, 152 insertions(+), 52 deletions(-) create mode 100644 api/info.ts diff --git a/api/client.ts b/api/client.ts index 8b3566cf..948a1708 100644 --- a/api/client.ts +++ b/api/client.ts @@ -1,36 +1,118 @@ -import { Api, Jellyfin } from "@jellyfin/sdk"; -import { getDeviceNameSync, getUniqueIdSync } from "react-native-device-info"; -import { name, version } from "../package.json" -import { capitalize } from "lodash"; +import { Api } from "@jellyfin/sdk/lib/api"; +import { JellyfinInfo } from "./info"; +import { JellifyServer } from "@/types/JellifyServer"; +import { JellifyUser } from "@/types/JellifyUser"; +import { storage } from '../constants/storage'; +import { MMKVStorageKeys } from "@/enums/mmkv-storage-keys"; +import uuid from 'react-native-uuid'; +import { JellifyLibrary } from "@/types/JellifyLibrary"; -/** - * Client object that represents Jellify on the Jellyfin server. - */ -export const jellifyClient: Jellyfin = new Jellyfin({ - clientInfo: { - name: capitalize(name), - version: version - }, - deviceInfo: { - name: getDeviceNameSync(), - id: getUniqueIdSync() + +export default class Client { + static #instance: Client; + + public api : Api | undefined; + public user : JellifyUser | undefined; + public server : JellifyServer | undefined; + public library : JellifyLibrary | undefined; + public sessionId : string = uuid.v4(); + + private constructor( + api?: Api | undefined, + user?: JellifyUser | undefined, + server?: JellifyServer | undefined, + library?: JellifyLibrary | undefined + ) { + + const userJson = storage.getString(MMKVStorageKeys.User) + const serverJson = storage.getString(MMKVStorageKeys.Server); + const libraryJson = storage.getString(MMKVStorageKeys.Library); + + + if (api) + this.api = api + + if (user) + this.setAndPersistUser + else if (userJson) + this.user = JSON.parse(userJson) + + if (server) + this.setAndPersistServer(server) + else if (serverJson) + this.server = JSON.parse(serverJson); + + if (library) + this.setAndPersistLibrary(library) + else if (libraryJson) + this.library = JSON.parse(libraryJson) } -}); -/** - * Uses the jellifyClient to create a public Jellyfin API instance. - * @param serverUrl The URL of the Jellyfin server - * @returns - */ -export function buildPublicApiClient(serverUrl : string) : Api { - return jellifyClient.createApi(serverUrl); -} + public static get instance(): Client { + if (!Client.#instance) { + Client.#instance = new Client(); + } -/** - * - * @param serverUrl The URL of the Jellyfin server - * @param accessToken The assigned accessToken for the Jellyfin user - */ -export function buildAuthenticatedApiClient(serverUrl: string, accessToken: string) : Api { - return jellifyClient.createApi(serverUrl, accessToken); + return Client.#instance; + } + + public static signOut(): void { + if (!Client.#instance) { + Client.instance; + } + + Client.instance.removeCredentials() + } + + private setAndPersistUser(user: JellifyUser) { + this.user = user; + + // persist user details + storage.set(MMKVStorageKeys.User, JSON.stringify(user)); + } + + private setAndPersistServer(server : JellifyServer) { + this.server = server; + + storage.set(MMKVStorageKeys.Server, JSON.stringify(server)); + } + + private setAndPersistLibrary(library : JellifyLibrary) { + this.library = library; + + storage.set(MMKVStorageKeys.Library, JSON.stringify(library)) + } + + private removeCredentials() { + this.library = undefined; + this.library = undefined; + this.server = undefined; + this.user = undefined; + + storage.delete(MMKVStorageKeys.Server) + storage.delete(MMKVStorageKeys.Library) + storage.delete(MMKVStorageKeys.User) + } + + /** + * Uses the jellifyClient to create a public Jellyfin API instance. + * @param serverUrl The URL of the Jellyfin server + * @returns + */ + public static setPublicApiClient(server : JellifyServer) : void { + const api = JellyfinInfo.createApi(server.url); + + Client.#instance = new Client(api, undefined, server, undefined) + } + + /** + * + * @param serverUrl The URL of the Jellyfin server + * @param accessToken The assigned accessToken for the Jellyfin user + */ + public static setPrivateApiClient(server : JellifyServer, user : JellifyUser) : void { + const api = JellyfinInfo.createApi(server.url, user.accessToken); + + Client.#instance = new Client(api, user, server, undefined) + } } \ No newline at end of file diff --git a/api/info.ts b/api/info.ts new file mode 100644 index 00000000..585ec13f --- /dev/null +++ b/api/info.ts @@ -0,0 +1,18 @@ +import { Api, Jellyfin } from "@jellyfin/sdk"; +import { getDeviceNameSync, getUniqueIdSync } from "react-native-device-info"; +import { name, version } from "../package.json" +import { capitalize } from "lodash"; + +/** + * Client object that represents Jellify on the Jellyfin server. + */ +export const JellyfinInfo: Jellyfin = new Jellyfin({ + clientInfo: { + name: capitalize(name), + version: version + }, + deviceInfo: { + name: getDeviceNameSync(), + id: getUniqueIdSync() + } +}); \ No newline at end of file diff --git a/api/queries/functions/api.ts b/api/queries/functions/api.ts index 0573a77a..2eafc9e0 100644 --- a/api/queries/functions/api.ts +++ b/api/queries/functions/api.ts @@ -1,5 +1,5 @@ import { Api } from "@jellyfin/sdk"; -import { jellifyClient } from "../../client"; +import { JellyfinInfo } from "../../info"; import _ from "lodash"; export function createApi(serverUrl?: string, username?: string, password?: string, accessToken?: string): Promise { @@ -12,22 +12,22 @@ export function createApi(serverUrl?: string, username?: string, password?: stri if (!_.isUndefined(accessToken)) { console.info("Creating API with accessToken") - return resolve(jellifyClient.createApi(serverUrl, accessToken)); + return resolve(JellyfinInfo.createApi(serverUrl, accessToken)); } if (_.isUndefined(username) && _.isUndefined(password)) { console.info("Creating public API for server url") - return resolve(jellifyClient.createApi(serverUrl)); + return resolve(JellyfinInfo.createApi(serverUrl)); } console.log("Signing into Jellyfin") - let authResult = await jellifyClient.createApi(serverUrl).authenticateUserByName(username!, password); + let authResult = await JellyfinInfo.createApi(serverUrl).authenticateUserByName(username!, password); if (authResult.data.AccessToken) { console.info("Signed into Jellyfin successfully") - return resolve(jellifyClient.createApi(serverUrl, authResult.data.AccessToken)); + return resolve(JellyfinInfo.createApi(serverUrl, authResult.data.AccessToken)); } return reject("Unable to sign in"); diff --git a/components/Login/helpers/server-address.tsx b/components/Login/helpers/server-address.tsx index eb630c28..41339143 100644 --- a/components/Login/helpers/server-address.tsx +++ b/components/Login/helpers/server-address.tsx @@ -11,7 +11,7 @@ import Input from "../../Global/helpers/input"; import Button from "../../Global/helpers/button"; import { http, https } from "../utils/constants"; import { storage } from "../../../constants/storage"; -import { jellifyClient } from "../../../api/client"; +import { JellyfinInfo } from "../../../api/info"; import { Jellyfin } from "@jellyfin/sdk/lib/jellyfin"; import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api"; import { SafeAreaView } from "react-native-safe-area-context"; @@ -25,7 +25,7 @@ export default function ServerAddress(): React.JSX.Element { const useServerMutation = useMutation({ mutationFn: async () => { - let jellyfin = new Jellyfin(jellifyClient); + let jellyfin = new JellyfinInfo(JellyfinInfo); if (!!!serverAddress) throw new Error("Server address was empty"); diff --git a/components/jellyfin-api-provider.tsx b/components/jellyfin-api-provider.tsx index 5db1468c..2ac6cd60 100644 --- a/components/jellyfin-api-provider.tsx +++ b/components/jellyfin-api-provider.tsx @@ -8,7 +8,7 @@ import { JellifyServer } from '../types/JellifyServer'; import { JellifyLibrary } from '../types/JellifyLibrary'; import { JellifyUser } from '../types/JellifyUser'; import uuid from 'react-native-uuid'; -import { buildAuthenticatedApiClient, buildPublicApiClient } from '@/api/client'; +import Client from '@/api/client'; interface JellyfinApiClientContext { apiClient: Api | undefined; @@ -24,19 +24,17 @@ interface JellyfinApiClientContext { const JellyfinApiClientContextInitializer = () => { - const userJson = storage.getString(MMKVStorageKeys.User) - const serverJson = storage.getString(MMKVStorageKeys.Server); - const libraryJson = storage.getString(MMKVStorageKeys.Library); - - const [sessionId, setSessionId] = useState(uuid.v4()) - const [user, setUser] = useState(userJson ? (JSON.parse(userJson) as JellifyUser) : undefined); - const [server, setServer] = useState(serverJson ? (JSON.parse(serverJson) as JellifyServer) : undefined); - const [library, setLibrary] = useState(libraryJson ? (JSON.parse(libraryJson) as JellifyLibrary) : undefined); - - const [apiClient, setApiClient] = useState(!isUndefined(server) && !isUndefined(user) ? buildAuthenticatedApiClient(server!.url, user!.accessToken) : undefined); + const [apiClient, setApiClient] = useState(Client.instance.api); + const [sessionId, setSessionId] = useState(Client.instance.sessionId); + const [user, setUser] = useState(Client.instance.user); + const [server, setServer] = useState(Client.instance.server); + const [library, setLibrary] = useState(Client.instance.library); const signOut = () => { console.debug("Signing out of Jellify"); + + Client.signOut(); + setUser(undefined); setServer(undefined); setLibrary(undefined); @@ -44,11 +42,11 @@ const JellyfinApiClientContextInitializer = () => { useEffect(() => { if (server && user) - setApiClient(buildAuthenticatedApiClient(server.url, user.accessToken)); + Client.setPrivateApiClient(server, user) else if (server) - setApiClient(buildPublicApiClient(server.url)); + Client.setPublicApiClient(server) else - setApiClient(undefined); + Client.signOut(); }, [ server, user diff --git a/index.js b/index.js index b40b214f..0c6395f8 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,8 @@ import App from './App'; import {name as appName} from './app.json'; import { PlaybackService } from './player/service' import TrackPlayer from 'react-native-track-player'; +import { Client } from './api/client'; +Client.instance; AppRegistry.registerComponent(appName, () => App); TrackPlayer.registerPlaybackService(() => PlaybackService); \ No newline at end of file