diff --git a/api/client.ts b/api/client.ts index 6564cc5a..dac6f81c 100644 --- a/api/client.ts +++ b/api/client.ts @@ -7,15 +7,14 @@ import { MMKVStorageKeys } from "@/enums/mmkv-storage-keys"; import uuid from 'react-native-uuid'; import { JellifyLibrary } from "@/types/JellifyLibrary"; - 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 api : Api | undefined; + private user : JellifyUser | undefined; + private server : JellifyServer | undefined; + private library : JellifyLibrary | undefined; + private sessionId : string = uuid.v4(); private constructor( api?: Api | undefined, @@ -31,7 +30,7 @@ export default class Client { if (user) - this.setAndPersistUser + this.setAndPersistUser(user) else if (userJson) this.user = JSON.parse(userJson) @@ -59,19 +58,36 @@ export default class Client { return Client.#instance; } - public static signOut(): void { - if (!Client.#instance) { - Client.instance; - } + public static get api(): Api | undefined { + return Client.#instance.api; + } - Client.instance.removeCredentials() + public static get server(): JellifyServer | undefined { + return Client.#instance.server; + } + + public static get user(): JellifyUser | undefined { + return Client.#instance.user; + } + + public static get library(): JellifyLibrary | undefined { + return Client.#instance.library; + } + + public static signOut(): void { + Client.#instance.removeCredentials() + } + + public static switchServer() : void { + Client.#instance.removeServer(); } public static switchUser(): void { - if (!Client.#instance) - Client.instance; + Client.#instance.removeUser(); + } - Client.instance.removeUser(); + public static setUser(user: JellifyUser): void { + Client.#instance.setAndPersistUser(user); } private setAndPersistUser(user: JellifyUser) { @@ -104,6 +120,12 @@ export default class Client { storage.delete(MMKVStorageKeys.User) } + private removeServer() { + this.server = undefined; + + storage.delete(MMKVStorageKeys.Server) + } + private removeUser() { this.user = undefined; @@ -129,6 +151,10 @@ export default class Client { public static setPrivateApiClient(server : JellifyServer, user : JellifyUser) : void { const api = JellyfinInfo.createApi(server.url, user.accessToken); - Client.#instance = new Client(api, user, server, undefined) + Client.#instance = new Client(api, user, server, undefined); + } + + public static setLibrary(library : JellifyLibrary) : void { + Client.instance.library = library } } \ No newline at end of file diff --git a/api/queries/artist.ts b/api/queries/artist.ts index a35fb4cf..5613ece7 100644 --- a/api/queries/artist.ts +++ b/api/queries/artist.ts @@ -1,13 +1,13 @@ import { useQuery } from "@tanstack/react-query" import { QueryKeys } from "../../enums/query-keys" -import { Api } from "@jellyfin/sdk" import { getItemsApi } from "@jellyfin/sdk/lib/utils/api" import { BaseItemKind, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models" +import Client from "../client" -export const useArtistAlbums = (artistId: string, api: Api) => useQuery({ - queryKey: [QueryKeys.ArtistAlbums, artistId, api], +export const useArtistAlbums = (artistId: string) => useQuery({ + queryKey: [QueryKeys.ArtistAlbums, artistId], queryFn: ({ queryKey }) => { - return getItemsApi(queryKey[2] as Api).getItems({ + return getItemsApi(Client.api!).getItems({ includeItemTypes: [BaseItemKind.MusicAlbum], recursive: true, excludeItemIds: [queryKey[1] as string], @@ -26,10 +26,10 @@ export const useArtistAlbums = (artistId: string, api: Api) => useQuery({ }) -export const useArtistFeaturedOnAlbums = (artistId: string, api: Api) => useQuery({ - queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId, api], +export const useArtistFeaturedOnAlbums = (artistId: string) => useQuery({ + queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId], queryFn: ({ queryKey }) => { - return getItemsApi(queryKey[2] as Api).getItems({ + return getItemsApi(Client.api!).getItems({ includeItemTypes: [BaseItemKind.MusicAlbum], recursive: true, excludeItemIds: [queryKey[1] as string], diff --git a/api/queries/favorites.ts b/api/queries/favorites.ts index 121776a0..03a39deb 100644 --- a/api/queries/favorites.ts +++ b/api/queries/favorites.ts @@ -3,34 +3,28 @@ import { Api } from "@jellyfin/sdk"; import { useQuery } from "@tanstack/react-query"; import { fetchFavoriteAlbums, fetchFavoriteArtists, fetchFavoriteTracks, fetchUserData } from "./functions/favorites"; -export const useFavoriteArtists = (api: Api, libraryId: string) => useQuery({ - queryKey: [QueryKeys.FavoriteArtists, api, libraryId], +export const useFavoriteArtists = () => useQuery({ + queryKey: [QueryKeys.FavoriteArtists], queryFn: () => { - return fetchFavoriteArtists(api, libraryId) + return fetchFavoriteArtists() } }); -export const useFavoriteAlbums = (api: Api, libraryId: string) => useQuery({ - queryKey: [QueryKeys.FavoriteAlbums, api, libraryId], - queryFn: ({ queryKey }) => { +export const useFavoriteAlbums = () => useQuery({ + queryKey: [QueryKeys.FavoriteAlbums], + queryFn: () => { - return fetchFavoriteAlbums(api, libraryId) + return fetchFavoriteAlbums() } }); -export const useFavoriteTracks = (api: Api, libraryId: string) => useQuery({ - queryKey: [QueryKeys.FavoriteTracks, api, libraryId], - queryFn: ({ queryKey }) => { - - return fetchFavoriteTracks(api, libraryId) - } +export const useFavoriteTracks = () => useQuery({ + queryKey: [QueryKeys.FavoriteTracks], + queryFn: () => fetchFavoriteTracks() }); -export const useUserData = (api: Api, itemId: string) => useQuery({ - queryKey: [QueryKeys.UserData, api, itemId], - queryFn: ({ queryKey }) => { - - return fetchUserData(api, itemId) - } -}) \ No newline at end of file +export const useUserData = (itemId: string) => useQuery({ + queryKey: [QueryKeys.UserData, itemId], + queryFn: () => fetchUserData(itemId) +}); \ No newline at end of file diff --git a/api/queries/functions/favorites.ts b/api/queries/functions/favorites.ts index 1dc815d9..0c935673 100644 --- a/api/queries/functions/favorites.ts +++ b/api/queries/functions/favorites.ts @@ -1,18 +1,18 @@ -import { Api } from "@jellyfin/sdk"; +import Client from "@/api/client"; import { BaseItemDto, BaseItemKind, ItemSortBy, SortOrder, UserItemDataDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -export function fetchFavoriteArtists(api: Api, musicLibraryId: string): Promise { +export function fetchFavoriteArtists(): Promise { console.debug(`Fetching user's favorite artists`); return new Promise(async (resolve, reject) => { - getItemsApi(api) + getItemsApi(Client.api!) .getItems({ includeItemTypes: [ BaseItemKind.MusicArtist ], isFavorite: true, - parentId: musicLibraryId, + parentId: Client.library!.musicLibraryId, recursive: true, sortBy: [ ItemSortBy.SortName @@ -35,17 +35,17 @@ export function fetchFavoriteArtists(api: Api, musicLibraryId: string): Promise< }) } -export function fetchFavoriteAlbums(api: Api, musicLibraryId: string): Promise { +export function fetchFavoriteAlbums(): Promise { console.debug(`Fetching user's favorite albums`); return new Promise(async (resolve, reject) => { - getItemsApi(api) + getItemsApi(Client.api!) .getItems({ includeItemTypes: [ BaseItemKind.MusicAlbum ], isFavorite: true, - parentId: musicLibraryId, + parentId: Client.library!.musicLibraryId!, recursive: true, sortBy: [ ItemSortBy.SortName @@ -68,17 +68,17 @@ export function fetchFavoriteAlbums(api: Api, musicLibraryId: string): Promise { +export function fetchFavoriteTracks(): Promise { console.debug(`Fetching user's favorite artists`); return new Promise(async (resolve, reject) => { - getItemsApi(api) + getItemsApi(Client.api!) .getItems({ includeItemTypes: [ BaseItemKind.Audio ], isFavorite: true, - parentId: musicLibraryId, + parentId: Client.library!.musicLibraryId, recursive: true, sortBy: [ ItemSortBy.SortName @@ -101,9 +101,9 @@ export function fetchFavoriteTracks(api: Api, musicLibraryId: string): Promise { +export function fetchUserData(itemId: string): Promise { return new Promise(async (resolve, reject) => { - getItemsApi(api) + getItemsApi(Client.api!) .getItemUserData({ itemId }).then((response) => { diff --git a/api/queries/functions/libraries.ts b/api/queries/functions/libraries.ts index 19245d9d..03e42fde 100644 --- a/api/queries/functions/libraries.ts +++ b/api/queries/functions/libraries.ts @@ -1,14 +1,14 @@ -import { Api } from "@jellyfin/sdk"; +import Client from "@/api/client"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api"; import { isUndefined } from "lodash"; -export function fetchMusicLibraries(api: Api): Promise { +export function fetchMusicLibraries(): Promise { return new Promise(async (resolve, reject) => { console.debug("Fetching music libraries from Jellyfin"); - let libraries = await getItemsApi(api).getItems({ + let libraries = await getItemsApi(Client.api!).getItems({ includeItemTypes: ['CollectionFolder'] }); @@ -24,11 +24,11 @@ export function fetchMusicLibraries(api: Api): Promise { }); } -export function fetchPlaylistLibrary(api: Api): Promise { +export function fetchPlaylistLibrary(): Promise { return new Promise(async (resolve, reject) => { console.debug("Fetching playlist library from Jellyfin"); - let libraries = await getItemsApi(api).getItems({ + let libraries = await getItemsApi(Client.api!).getItems({ includeItemTypes: ['ManualPlaylistsFolder'], excludeItemTypes: ['CollectionFolder'] }); diff --git a/api/queries/functions/playlists.ts b/api/queries/functions/playlists.ts index 33e5ca12..7813d691 100644 --- a/api/queries/functions/playlists.ts +++ b/api/queries/functions/playlists.ts @@ -1,15 +1,16 @@ +import Client from "@/api/client"; import { Api } from "@jellyfin/sdk"; import { BaseItemDto, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; -export function fetchUserPlaylists(api: Api, userId: string, playlistLibraryId: string): Promise { +export function fetchUserPlaylists(): Promise { console.debug("Fetching user playlists"); return new Promise(async (resolve, reject) => { - getItemsApi(api) + getItemsApi(Client.api!) .getItems({ - userId: userId, - parentId: playlistLibraryId, + userId: Client.user!.id, + parentId: Client.library!.playlistLibraryId!, fields: [ "Path" ], @@ -36,13 +37,13 @@ export function fetchUserPlaylists(api: Api, userId: string, playlistLibraryId: }) } -export function fetchPublicPlaylists(api: Api, playlistLibraryId: string): Promise { +export function fetchPublicPlaylists(): Promise { console.debug("Fetching public playlists"); return new Promise(async (resolve, reject) => { - getItemsApi(api) + getItemsApi(Client.api!) .getItems({ - parentId: playlistLibraryId, + parentId: Client.library!.playlistLibraryId!, sortBy: [ ItemSortBy.IsFolder, ItemSortBy.SortName diff --git a/api/queries/functions/recents.ts b/api/queries/functions/recents.ts index be391112..cf82bd07 100644 --- a/api/queries/functions/recents.ts +++ b/api/queries/functions/recents.ts @@ -8,13 +8,13 @@ export function fetchRecentlyPlayed(): Promise { console.debug("Fetching recently played items"); return new Promise(async (resolve, reject) => { - getItemsApi(Client.instance.api!) + getItemsApi(Client.api!) .getItems({ includeItemTypes: [ BaseItemKind.Audio ], limit: queryConfig.limits.recents, - parentId: Client.instance.library!.musicLibraryId, + parentId: Client.library!.musicLibraryId, recursive: true, sortBy: [ ItemSortBy.DatePlayed @@ -37,4 +37,17 @@ export function fetchRecentlyPlayed(): Promise { reject(error); }) }) +} + +export function fetchRecentlyPlayedArtists() : Promise { + return fetchRecentlyPlayed() + .then((tracks) => { + return getItemsApi(Client.api!) + .getItems({ + ids: tracks.map(track => track.ArtistItems![0].Id!) + }) + .then((recentArtists) => { + return recentArtists.data.Items! + }); + }); } \ No newline at end of file diff --git a/api/queries/libraries.ts b/api/queries/libraries.ts index 46e6d1c8..14462079 100644 --- a/api/queries/libraries.ts +++ b/api/queries/libraries.ts @@ -3,22 +3,12 @@ import { Api } from "@jellyfin/sdk"; import { useQuery } from "@tanstack/react-query"; import { fetchMusicLibraries, fetchPlaylistLibrary } from "./functions/libraries"; -export const useMusicLibraries = (api: Api) => useQuery({ - queryKey: [QueryKeys.Libraries, api], - queryFn: async ({ queryKey }) => { - - const api : Api = queryKey[1] as Api; - - return await fetchMusicLibraries(api) - } +export const useMusicLibraries = () => useQuery({ + queryKey: [QueryKeys.Libraries], + queryFn: () => fetchMusicLibraries() }); -export const usePlaylistLibrary = (api: Api) => useQuery({ - queryKey: [QueryKeys.Playlist, api], - queryFn: async ({ queryKey }) => { - - const api : Api = queryKey[1] as Api; - - return await fetchPlaylistLibrary(api) - } +export const usePlaylistLibrary = () => useQuery({ + queryKey: [QueryKeys.Playlist], + queryFn: () => fetchPlaylistLibrary() }); \ No newline at end of file diff --git a/api/queries/playlist.ts b/api/queries/playlist.ts index fcd0f4a7..d5f851aa 100644 --- a/api/queries/playlist.ts +++ b/api/queries/playlist.ts @@ -3,14 +3,8 @@ import { Api } from "@jellyfin/sdk"; import { useQuery } from "@tanstack/react-query"; import { fetchUserPlaylists } from "./functions/playlists"; -export const useUserPlaylists = (api: Api, userId: string, playlistLibraryId: string) => useQuery({ - queryKey: [QueryKeys.UserPlaylists, api, userId, playlistLibraryId], - queryFn: ({ queryKey }) => { - const api: Api = queryKey[1] as Api; - const userId: string = queryKey[2] as string; - const playlistLibraryId: string = queryKey[3] as string; - - return fetchUserPlaylists(api, userId, playlistLibraryId); - } -}) +export const useUserPlaylists = () => useQuery({ + queryKey: [QueryKeys.UserPlaylists], + queryFn: () => fetchUserPlaylists() +}); diff --git a/api/queries/recently-played.ts b/api/queries/recently-played.ts index ad1e71d4..e4d515f3 100644 --- a/api/queries/recently-played.ts +++ b/api/queries/recently-played.ts @@ -1,29 +1,15 @@ import { useQuery } from "@tanstack/react-query"; import { QueryKeys } from "../../enums/query-keys"; -import { fetchRecentlyPlayed } from "./functions/recents"; +import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from "./functions/recents"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api" import Client from "../client"; export const useRecentlyPlayed = () => useQuery({ queryKey: [QueryKeys.RecentlyPlayed], - queryFn: () => { - - return fetchRecentlyPlayed() - } + queryFn: () => fetchRecentlyPlayed() }); export const useRecentlyPlayedArtists = () => useQuery({ queryKey: [QueryKeys.RecentlyPlayedArtists], - queryFn: () => { - return fetchRecentlyPlayed() - .then((tracks) => { - return getItemsApi(Client.instance.api!) - .getItems({ - ids: tracks.map(track => track.ArtistItems![0].Id!) - }) - .then((recentArtists) => { - return recentArtists.data.Items! - }); - }); - } + queryFn: () => fetchRecentlyPlayedArtists() }); \ No newline at end of file diff --git a/api/queries/tracks.ts b/api/queries/tracks.ts index 8736e3bd..6a0046ce 100644 --- a/api/queries/tracks.ts +++ b/api/queries/tracks.ts @@ -1,16 +1,15 @@ import { QueryKeys } from "@/enums/query-keys"; -import { Api } from "@jellyfin/sdk"; import { ItemSortBy } from "@jellyfin/sdk/lib/generated-client/models/item-sort-by"; import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api"; import { useQuery } from "@tanstack/react-query"; import { queryConfig } from "./query.config"; +import Client from "../client"; -export const useItemTracks = (itemId: string, api: Api, sort: boolean = false) => useQuery({ - queryKey: [QueryKeys.ItemTracks, itemId, api, sort], +export const useItemTracks = (itemId: string, sort: boolean = false) => useQuery({ + queryKey: [QueryKeys.ItemTracks, itemId, sort], queryFn: ({ queryKey }) => { const itemId : string = queryKey[1] as string; - const api : Api = queryKey[2] as Api; const sort : boolean = queryKey[3] as boolean; let sortBy: ItemSortBy[] = []; @@ -23,7 +22,7 @@ export const useItemTracks = (itemId: string, api: Api, sort: boolean = false) = ] } - return getItemsApi(api).getItems({ + return getItemsApi(Client.api!).getItems({ parentId: itemId, sortBy }) diff --git a/components/Album/component.tsx b/components/Album/component.tsx index d5a78e92..16199573 100644 --- a/components/Album/component.tsx +++ b/components/Album/component.tsx @@ -3,7 +3,6 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { ScrollView, YStack, XStack } from "tamagui"; import { CachedImage } from "@georstat/react-native-image-cache"; import { getImageApi } from "@jellyfin/sdk/lib/utils/api"; -import { useApiClientContext } from "../jellyfin-api-provider"; import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models"; import { queryConfig } from "../../api/queries/query.config"; import { H4, H5, Text } from "../Global/helpers/text"; @@ -15,6 +14,7 @@ import { useItemTracks } from "@/api/queries/tracks"; import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context"; import FavoriteHeaderButton from "../Global/components/favorite-header-button"; import { useEffect } from "react"; +import Client from "@/api/client"; interface AlbumProps { album: BaseItemDto, @@ -31,12 +31,11 @@ export default function Album(props: AlbumProps): React.JSX.Element { } }) - const { apiClient } = useApiClientContext(); const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext(); const { width } = useSafeAreaFrame(); - const { data: tracks, isLoading, refetch } = useItemTracks(props.album.Id!, apiClient!, true); + const { data: tracks, isLoading, refetch } = useItemTracks(props.album.Id!, true); useEffect(() => { refetch(); @@ -49,7 +48,7 @@ export default function Album(props: AlbumProps): React.JSX.Element { (2); - const { apiClient } = useApiClientContext(); - const { height, width } = useSafeAreaFrame(); const bannerHeight = height / 6; - const { data: albums } = useArtistAlbums(props.artist.Id!, apiClient!); + const { data: albums } = useArtistAlbums(props.artist.Id!); return ( @@ -46,7 +44,7 @@ export default function Artist(props: ArtistProps): React.JSX.Element { alignContent="center"> void }) : React.JSX.Element { - - const { apiClient } = useApiClientContext(); - const [isFavorite, setIsFavorite] = useState(isFavoriteItem(item)); - const { data, isFetching, isFetched, refetch } = useUserData(apiClient!, item.Id!); + const { data, isFetching, isFetched, refetch } = useUserData(item.Id!); const useSetFavorite = useMutation({ mutationFn: async (mutation: SetFavoriteMutation) => { - return getUserLibraryApi(mutation.api) + return getUserLibraryApi(Client.api!) .markFavoriteItem({ itemId: mutation.item.Id! }) @@ -46,7 +42,7 @@ export default function FavoriteHeaderButton({ const useRemoveFavorite = useMutation({ mutationFn: async (mutation: SetFavoriteMutation) => { - return getUserLibraryApi(mutation.api) + return getUserLibraryApi(Client.api!) .unmarkFavoriteItem({ itemId: mutation.item.Id! }) @@ -58,9 +54,9 @@ export default function FavoriteHeaderButton({ const toggleFavorite = () => { if (isFavorite) - useRemoveFavorite.mutate({ item, api: apiClient!}) + useRemoveFavorite.mutate({ item }) else - useSetFavorite.mutate({ item, api: apiClient! }) + useSetFavorite.mutate({ item }) } useEffect(() => { diff --git a/components/Global/components/track.tsx b/components/Global/components/track.tsx index 844cf26d..4a6af972 100644 --- a/components/Global/components/track.tsx +++ b/components/Global/components/track.tsx @@ -7,12 +7,12 @@ import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/model import { Colors } from "@/enums/colors"; import { CachedImage } from "@georstat/react-native-image-cache"; import { getImageApi } from "@jellyfin/sdk/lib/utils/api/image-api"; -import { useApiClientContext } from "@/components/jellyfin-api-provider"; import { queryConfig } from "@/api/queries/query.config"; import { useSafeAreaFrame } from "react-native-safe-area-context"; import Icon from "../helpers/icon"; import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { StackParamList } from "@/components/types"; +import Client from "@/api/client"; interface TrackProps { track: BaseItemDto; @@ -42,7 +42,6 @@ export default function Track({ }) : React.JSX.Element { const { width } = useSafeAreaFrame(); - const { apiClient } = useApiClientContext(); const { nowPlaying, usePlayNewQueue } = usePlayerContext(); const isPlaying = nowPlaying?.item.Id === track.Id; @@ -76,7 +75,7 @@ export default function Track({ > { showArtwork ? ( - + { props.children && ( diff --git a/components/Global/helpers/blurhash-loading.tsx b/components/Global/helpers/blurhash-loading.tsx index df3c0da0..aa5ad1a6 100644 --- a/components/Global/helpers/blurhash-loading.tsx +++ b/components/Global/helpers/blurhash-loading.tsx @@ -1,12 +1,33 @@ +import Client from "@/api/client"; +import { useItemImage } from "@/api/queries/image"; import { Blurhash } from "react-native-blurhash"; -import { ImageSourcePropType } from "react-native/Libraries/Image/Image"; +import { Image, View } from "tamagui"; -const BlurhashLoading = (props: any) => { - return ( - - - - ) +interface BlurhashLoadingProps { + itemId: string; + blurhash: string; + size: number } -export default BlurhashLoading; \ No newline at end of file +export default function BlurhashLoading(props: BlurhashLoadingProps) : React.JSX.Element { + + const { data: image, isSuccess } = useItemImage(Client.api!, props.itemId); + + return ( + + + { isSuccess ? ( + + ) : ( + + ) + } + + ) +} \ No newline at end of file diff --git a/components/Global/helpers/item-card.tsx b/components/Global/helpers/item-card.tsx index 8b6e4899..c7b311da 100644 --- a/components/Global/helpers/item-card.tsx +++ b/components/Global/helpers/item-card.tsx @@ -1,7 +1,6 @@ import React, { } from "react"; import type { CardProps as TamaguiCardProps } from "tamagui" import { H5, Card as TamaguiCard, View } from "tamagui"; -import { useApiClientContext } from "../../jellyfin-api-provider"; import { getImageApi } from "@jellyfin/sdk/lib/utils/api"; import { ImageType } from "@jellyfin/sdk/lib/generated-client/models"; import { CachedImage } from "@georstat/react-native-image-cache"; @@ -9,6 +8,7 @@ import invert from "invert-color" import { Blurhash } from "react-native-blurhash" import { queryConfig } from "../../../api/queries/query.config"; import { Text } from "./text"; +import Client from "@/api/client"; interface CardProps extends TamaguiCardProps { artistName?: string; @@ -21,14 +21,12 @@ interface CardProps extends TamaguiCardProps { export function ItemCard(props: CardProps) { - const { apiClient } = useApiClientContext(); - const dimensions = props.width && typeof(props.width) === "number" ? { width: props.width, height: props.width } : { width: 150, height: 150 }; const cardTextColor = props.blurhash ? invert(Blurhash.getAverageColor(props.blurhash)!, true) : undefined; const logoDimensions = props.width && typeof(props.width) === "number" ? { width: props.width / 2, height: props.width / 2 }: { width: 100, height: 100 }; - const cardLogoSource = getImageApi(apiClient!).getItemImageUrlById(props.itemId, ImageType.Logo); + const cardLogoSource = getImageApi(Client.api!).getItemImageUrlById(props.itemId, ImageType.Logo); return ( diff --git a/components/Home/provider.tsx b/components/Home/provider.tsx index 61f002ce..d8ff3a99 100644 --- a/components/Home/provider.tsx +++ b/components/Home/provider.tsx @@ -1,6 +1,7 @@ import React, { createContext, ReactNode, useContext, useState } from "react"; import { useRecentlyPlayed, useRecentlyPlayedArtists } from "../../api/queries/recently-played"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useQueries } from "@tanstack/react-query"; interface HomeContext { refreshing: boolean; diff --git a/components/Login/helpers/server-address.tsx b/components/Login/helpers/server-address.tsx index 41339143..428c592a 100644 --- a/components/Login/helpers/server-address.tsx +++ b/components/Login/helpers/server-address.tsx @@ -3,7 +3,6 @@ import _ from "lodash"; import { useMutation } from "@tanstack/react-query"; import { MMKVStorageKeys } from "../../../enums/mmkv-storage-keys"; import { JellifyServer } from "../../../types/JellifyServer"; -import { useApiClientContext } from "../../jellyfin-api-provider"; import { Spacer, Spinner, View, XStack, ZStack } from "tamagui"; import { SwitchWithLabel } from "../../Global/helpers/switch-with-label"; import { H1 } from "../../Global/helpers/text"; @@ -15,22 +14,21 @@ 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"; +import Client from "@/api/client"; export default function ServerAddress(): React.JSX.Element { - const { setServer } = useApiClientContext(); - const [useHttps, setUseHttps] = useState(true); const [serverAddress, setServerAddress] = useState(undefined); const useServerMutation = useMutation({ mutationFn: async () => { - let jellyfin = new JellyfinInfo(JellyfinInfo); + let jellyfin = new Jellyfin(JellyfinInfo); if (!!!serverAddress) throw new Error("Server address was empty"); - let api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`); + let api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`); return getSystemApi(api).getPublicSystemInfo(); }, @@ -41,7 +39,7 @@ export default function ServerAddress(): React.JSX.Element { console.debug("REMOVE THIS::onSuccess variable", publicSystemInfoResponse.data); console.log(`Connected to Jellyfin ${publicSystemInfoResponse.data.Version!}`); - let jellifyServer: JellifyServer = { + const server: JellifyServer = { url: `${useHttps ? https : http}${serverAddress!}`, address: serverAddress!, name: publicSystemInfoResponse.data.ServerName!, @@ -49,11 +47,11 @@ export default function ServerAddress(): React.JSX.Element { startUpComplete: publicSystemInfoResponse.data.StartupWizardCompleted! } - setServer(jellifyServer); + Client.setPublicApiClient(server); }, onError: async (error: Error) => { console.error("An error occurred connecting to the Jellyfin instance", error); - return storage.set(MMKVStorageKeys.Server, ""); + Client.signOut(); } }); diff --git a/components/Login/helpers/server-authentication.tsx b/components/Login/helpers/server-authentication.tsx index 1381a073..131fa00c 100644 --- a/components/Login/helpers/server-authentication.tsx +++ b/components/Login/helpers/server-authentication.tsx @@ -1,6 +1,5 @@ import React from "react"; import { useMutation } from "@tanstack/react-query"; -import { useApiClientContext } from "../../jellyfin-api-provider"; import _ from "lodash"; import { JellyfinCredentials } from "../../../api/types/jellyfin-credentials"; import { Spinner, View, YStack, ZStack } from "tamagui"; @@ -9,16 +8,15 @@ import { H1 } from "../../Global/helpers/text"; import Button from "../../Global/helpers/button"; import Input from "../../Global/helpers/input"; import { SafeAreaView } from "react-native-safe-area-context"; +import Client from "@/api/client"; export default function ServerAuthentication(): React.JSX.Element { const { username, setUsername } = useAuthenticationContext(); const [password, setPassword] = React.useState(''); - const { server, setServer, setUser, apiClient } = useApiClientContext(); - const useApiMutation = useMutation({ mutationFn: async (credentials: JellyfinCredentials) => { - return await apiClient!.authenticateUserByName(credentials.username, credentials.password!); + return await Client.api!.authenticateUserByName(credentials.username, credentials.password!); }, onSuccess: async (authResult) => { @@ -33,7 +31,7 @@ export default function ServerAuthentication(): React.JSX.Element { return Promise.reject(new Error("Unable to login")); console.log(`Successfully signed in to server`) - return setUser({ + return Client.setUser({ id: authResult.data.User!.Id!, name: authResult.data.User!.Name!, accessToken: (authResult.data.AccessToken as string) @@ -41,19 +39,16 @@ export default function ServerAuthentication(): React.JSX.Element { }, onError: async (error: Error) => { console.error("An error occurred connecting to the Jellyfin instance", error); - return Promise.reject(`An error occured signing into ${server!.name}`); + return Promise.reject(`An error occured signing into ${Client.server!.name}`); } }); return (

- { `Sign in to ${server?.name ?? "Jellyfin"}`} + { `Sign in to ${Client.server!.name ?? "Jellyfin"}`}

- @@ -81,7 +76,7 @@ export default function ServerAuthentication(): React.JSX.Element { onPress={() => { if (!_.isUndefined(username)) { - console.log(`Signing in to ${server!.name}`); + console.log(`Signing in to ${Client.server!.name}`); useApiMutation.mutate({ username, password }); } }} diff --git a/components/Login/helpers/server-library.tsx b/components/Login/helpers/server-library.tsx index cb840050..daf59453 100644 --- a/components/Login/helpers/server-library.tsx +++ b/components/Login/helpers/server-library.tsx @@ -1,5 +1,4 @@ import React, { useEffect } from "react"; -import { useApiClientContext } from "../../jellyfin-api-provider"; import { Spinner, Text, ToggleGroup, View } from "tamagui"; import { useAuthenticationContext } from "../provider"; import { H1, Label } from "../../Global/helpers/text"; @@ -7,27 +6,14 @@ import Button from "../../Global/helpers/button"; import _ from "lodash"; import { useMusicLibraries, usePlaylistLibrary } from "@/api/queries/libraries"; import { SafeAreaView } from "react-native-safe-area-context"; +import Client from "@/api/client"; export default function ServerLibrary(): React.JSX.Element { const { libraryId, setLibraryId } = useAuthenticationContext(); - const { apiClient, setUser, setLibrary } = useApiClientContext(); - const { data : libraries, isError, isPending, refetch: refetchMusicLibraries } = useMusicLibraries(apiClient!); - const { data : playlistLibrary, refetch: refetchPlaylistLibrary } = usePlaylistLibrary(apiClient!); - - useEffect(() => { - refetchMusicLibraries(); - refetchPlaylistLibrary(); - }, [ - apiClient - ]) - - useEffect(() => { - console.log(libraries) - }, [ - libraries - ]) + const { data : libraries, isError, isPending, refetch: refetchMusicLibraries } = useMusicLibraries(); + const { data : playlistLibrary, refetch: refetchPlaylistLibrary } = usePlaylistLibrary(); return ( @@ -59,19 +45,18 @@ export default function ServerLibrary(): React.JSX.Element { - diff --git a/components/Player/mini-player.tsx b/components/Player/mini-player.tsx index a602c115..436c4f00 100644 --- a/components/Player/mini-player.tsx +++ b/components/Player/mini-player.tsx @@ -39,7 +39,7 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers { refetch(); @@ -39,7 +39,7 @@ export default function Playlist(props: PlaylistProps): React.JSX.Element { Access Token - {apiClient!.accessToken} + {Client.api!!.accessToken}
Jellyfin Server
- {apiClient!.basePath} + {Client.api!.basePath}
diff --git a/components/Tracks/component.tsx b/components/Tracks/component.tsx index bbda5092..d127c92b 100644 --- a/components/Tracks/component.tsx +++ b/components/Tracks/component.tsx @@ -8,7 +8,7 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack"; export default function Tracks({ navigation }: { navigation: NativeStackNavigationProp }) : React.JSX.Element { const { apiClient, library } = useApiClientContext(); - const { data: tracks, refetch, isPending } = useFavoriteTracks(apiClient!, library!.musicLibraryId); + const { data: tracks, refetch, isPending } = useFavoriteTracks(); const { width } = useSafeAreaFrame(); diff --git a/components/jellify.tsx b/components/jellify.tsx index 47962431..bbc4ffb8 100644 --- a/components/jellify.tsx +++ b/components/jellify.tsx @@ -1,6 +1,5 @@ import _ from "lodash"; -import { JellyfinApiClientProvider, useApiClientContext } from "./jellyfin-api-provider"; -import React from "react"; +import React, { useEffect } from "react"; import { NavigationContainer } from "@react-navigation/native"; import Navigation from "./navigation"; import Login from "./Login/component"; @@ -10,29 +9,31 @@ import { JellifyDarkTheme, JellifyLightTheme } from "./theme"; import { PlayerProvider } from "../player/provider"; import { useColorScheme } from "react-native"; import { PortalProvider } from "tamagui"; +import Client from "@/api/client"; export default function Jellify(): React.JSX.Element { return ( - - - - - + + + ); } function App(): React.JSX.Element { - // If library hasn't been set, we haven't completed the auth flow - const { server, library } = useApiClientContext(); - const isDarkMode = useColorScheme() === "dark"; + useEffect(() => { + console.debug("Client instance changed") + }, [ + Client.instance + ]) + return ( - { server && library ? ( + { Client.user && Client.user ? ( diff --git a/components/jellyfin-api-provider.tsx b/components/jellyfin-api-provider.tsx deleted file mode 100644 index 2ac6cd60..00000000 --- a/components/jellyfin-api-provider.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { Api } from '@jellyfin/sdk'; -import React, { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react'; -import { useApi } from '../api/queries'; -import { isUndefined } from 'lodash'; -import { storage } from '../constants/storage'; -import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'; -import { JellifyServer } from '../types/JellifyServer'; -import { JellifyLibrary } from '../types/JellifyLibrary'; -import { JellifyUser } from '../types/JellifyUser'; -import uuid from 'react-native-uuid'; -import Client from '@/api/client'; - -interface JellyfinApiClientContext { - apiClient: Api | undefined; - sessionId: string; - server: JellifyServer | undefined; - setServer: React.Dispatch>; - user: JellifyUser | undefined; - setUser: React.Dispatch>; - library: JellifyLibrary | undefined; - setLibrary: React.Dispatch>; - signOut: () => void -} - -const JellyfinApiClientContextInitializer = () => { - - 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); - } - - useEffect(() => { - if (server && user) - Client.setPrivateApiClient(server, user) - else if (server) - Client.setPublicApiClient(server) - else - Client.signOut(); - }, [ - server, - user - ]); - - useEffect(() => { - if (server) { - console.debug("Storing new server configuration") - storage.set(MMKVStorageKeys.Server, JSON.stringify(server)) - } - else { - console.debug("Deleting server configuration from storage"); - storage.delete(MMKVStorageKeys.Server) - } - }, [ - server - ]) - - useEffect(() => { - if (user) { - console.debug("Storing new user profile") - storage.set(MMKVStorageKeys.User, JSON.stringify(user)); - } - else { - console.debug("Deleting access token from storage"); - storage.delete(MMKVStorageKeys.User); - } - }, [ - user - ]) - - useEffect(() => { - console.debug("Library changed") - if (library) { - console.debug("Setting library"); - storage.set(MMKVStorageKeys.Library, JSON.stringify(library)); - } else - storage.delete(MMKVStorageKeys.Library) - }, [ - library - ]) - - return { - apiClient, - sessionId, - server, - setServer, - user, - setUser, - library, - setLibrary, - signOut - }; -} - -export const JellyfinApiClientContext = - createContext({ - apiClient: undefined, - sessionId: "", - server: undefined, - setServer: () => {}, - user: undefined, - setUser: () => {}, - library: undefined, - setLibrary: () => {}, - signOut: () => {} - }); - -export const JellyfinApiClientProvider: ({ children }: { - children: ReactNode; -}) => React.JSX.Element = ({ children }: { children: ReactNode }) => { - const { - apiClient, - sessionId, - server, - setServer, - user, - setUser, - library, - setLibrary, - signOut - } = JellyfinApiClientContextInitializer(); - - // Add your logic to check if credentials are stored and initialize the API client here. - - return ( - - {children} - - ); -}; - -export const useApiClientContext = () => useContext(JellyfinApiClientContext) \ No newline at end of file diff --git a/player/provider.tsx b/player/provider.tsx index 2f349c1e..a367aadb 100644 --- a/player/provider.tsx +++ b/player/provider.tsx @@ -17,6 +17,7 @@ import { QueuingType } from "@/enums/queuing-type"; import { trigger } from "react-native-haptic-feedback"; import { getQueue, pause, seekTo, skip, skipToNext, skipToPrevious } from "react-native-track-player/lib/src/trackPlayer"; import { convertRunTimeTicksToSeconds } from "@/helpers/runtimeticks"; +import Client from "@/api/client"; interface PlayerContext { showPlayer: boolean; @@ -41,8 +42,7 @@ const PlayerContextInitializer = () => { const queueJson = storage.getString(MMKVStorageKeys.PlayQueue); - const { apiClient, sessionId } = useApiClientContext(); - const playStateApi = getPlaystateApi(apiClient!) + const playStateApi = getPlaystateApi(Client.api!) //#region State const [showPlayer, setShowPlayer] = useState(false); @@ -147,11 +147,11 @@ const PlayerContextInitializer = () => { setIsSkipping(true); // Optimistically set now playing - setNowPlaying(mapDtoToTrack(apiClient!, sessionId, mutation.tracklist[mutation.index ?? 0], QueuingType.FromSelection)); + setNowPlaying(mapDtoToTrack(Client.api!, sessionId, mutation.tracklist[mutation.index ?? 0], QueuingType.FromSelection)); await resetQueue(false); await addToQueue(mutation.tracklist.map((track) => { - return mapDtoToTrack(apiClient!, sessionId, track, QueuingType.FromSelection) + return mapDtoToTrack(Co!, sessionId, track, QueuingType.FromSelection) })); setQueueName(mutation.queueName);