converting api client to be a singleton, this means we can get rid of a lot of code

This commit is contained in:
Violet Caulfield
2025-01-19 13:35:58 -06:00
parent 69b90d2818
commit 4e9f84b216
33 changed files with 215 additions and 382 deletions

View File

@@ -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
}
}

View File

@@ -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],

View File

@@ -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)
}
})
export const useUserData = (itemId: string) => useQuery({
queryKey: [QueryKeys.UserData, itemId],
queryFn: () => fetchUserData(itemId)
});

View File

@@ -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<BaseItemDto[]> {
export function fetchFavoriteArtists(): Promise<BaseItemDto[]> {
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<BaseItemDto[]> {
export function fetchFavoriteAlbums(): Promise<BaseItemDto[]> {
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<B
})
}
export function fetchFavoriteTracks(api: Api, musicLibraryId: string): Promise<BaseItemDto[]> {
export function fetchFavoriteTracks(): Promise<BaseItemDto[]> {
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<B
})
}
export function fetchUserData(api: Api, itemId: string): Promise<UserItemDataDto> {
export function fetchUserData(itemId: string): Promise<UserItemDataDto> {
return new Promise(async (resolve, reject) => {
getItemsApi(api)
getItemsApi(Client.api!)
.getItemUserData({
itemId
}).then((response) => {

View File

@@ -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<BaseItemDto[]> {
export function fetchMusicLibraries(): Promise<BaseItemDto[]> {
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<BaseItemDto[]> {
});
}
export function fetchPlaylistLibrary(api: Api): Promise<BaseItemDto> {
export function fetchPlaylistLibrary(): Promise<BaseItemDto> {
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']
});

View File

@@ -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<BaseItemDto[]> {
export function fetchUserPlaylists(): Promise<BaseItemDto[]> {
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<BaseItemDto[]> {
export function fetchPublicPlaylists(): Promise<BaseItemDto[]> {
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

View File

@@ -8,13 +8,13 @@ export function fetchRecentlyPlayed(): Promise<BaseItemDto[]> {
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<BaseItemDto[]> {
reject(error);
})
})
}
export function fetchRecentlyPlayedArtists() : Promise<BaseItemDto[]> {
return fetchRecentlyPlayed()
.then((tracks) => {
return getItemsApi(Client.api!)
.getItems({
ids: tracks.map(track => track.ArtistItems![0].Id!)
})
.then((recentArtists) => {
return recentArtists.data.Items!
});
});
}

View File

@@ -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()
});

View File

@@ -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()
});

View File

@@ -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()
});

View File

@@ -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
})

View File

@@ -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 {
<ScrollView contentInsetAdjustmentBehavior="automatic">
<YStack alignItems="center" minHeight={width / 1.1}>
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
props.album.Id!,
ImageType.Primary,

View File

@@ -1,13 +1,11 @@
import { useFavoriteAlbums } from "@/api/queries/favorites";
import { AlbumsProps } from "../types";
import { useApiClientContext } from "../jellyfin-api-provider";
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
import { ItemCard } from "../Global/helpers/item-card";
import { FlatList, RefreshControl } from "react-native";
export default function Albums({ navigation }: AlbumsProps) : React.JSX.Element {
const { apiClient, library } = useApiClientContext();
const { data: albums, refetch, isPending } = useFavoriteAlbums(apiClient!, library!.musicLibraryId);
const { data: albums, refetch, isPending } = useFavoriteAlbums();
const { width } = useSafeAreaFrame();

View File

@@ -1,6 +1,5 @@
import { ScrollView, YStack } from "tamagui";
import { useArtistAlbums } from "../../api/queries/artist";
import { useApiClientContext } from "../jellyfin-api-provider";
import { FlatList } from "react-native";
import { ItemCard } from "../Global/helpers/item-card";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
@@ -13,6 +12,7 @@ import { queryConfig } from "@/api/queries/query.config";
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
import FavoriteHeaderButton from "../Global/components/favorite-header-button";
import Client from "@/api/client";
interface ArtistProps {
artist: BaseItemDto
@@ -31,13 +31,11 @@ export default function Artist(props: ArtistProps): React.JSX.Element {
const [columns, setColumns] = useState<number>(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 (
<SafeAreaView style={{ flex: 1 }} edges={["top", "right", "left"]}>
@@ -46,7 +44,7 @@ export default function Artist(props: ArtistProps): React.JSX.Element {
alignContent="center">
<YStack alignContent="center" justifyContent="center" minHeight={bannerHeight}>
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
props.artist.Id!,
ImageType.Primary,

View File

@@ -1,6 +1,5 @@
import { useFavoriteArtists } from "@/api/queries/favorites";
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
import { useApiClientContext } from "../jellyfin-api-provider";
import React from "react";
import { FlatList, RefreshControl } from "react-native";
import { ItemCard } from "../Global/helpers/item-card";
@@ -8,8 +7,7 @@ import { ArtistsProps } from "../types";
export default function Artists({ navigation }: ArtistsProps): React.JSX.Element {
const { apiClient, library } = useApiClientContext();
const { data: artists, refetch, isPending } = useFavoriteArtists(apiClient!, library!.musicLibraryId);
const { data: artists, refetch, isPending } = useFavoriteArtists();
const { width } = useSafeAreaFrame();

View File

@@ -2,17 +2,16 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import React, { useEffect, useState } from "react";
import Icon from "../helpers/icon";
import { Colors } from "@/enums/colors";
import { useApiClientContext } from "@/components/jellyfin-api-provider";
import { Api } from "@jellyfin/sdk";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useMutation, useQuery } from "@tanstack/react-query";
import { isUndefined } from "lodash";
import { useUserData } from "@/api/queries/favorites";
import { Spinner } from "tamagui";
import Client from "@/api/client";
interface SetFavoriteMutation {
item: BaseItemDto,
api: Api
}
export default function FavoriteHeaderButton({
@@ -23,16 +22,13 @@ export default function FavoriteHeaderButton({
onToggle?: () => void
}) : React.JSX.Element {
const { apiClient } = useApiClientContext();
const [isFavorite, setIsFavorite] = useState<boolean>(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(() => {

View File

@@ -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 ? (
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
track.AlbumId ?? "",
ImageType.Primary,

View File

@@ -1,8 +1,8 @@
import type { AvatarProps as TamaguiAvatarProps } from "tamagui";
import { Avatar as TamaguiAvatar, YStack } from "tamagui"
import { Text } from "./text"
import { useApiClientContext } from "@/components/jellyfin-api-provider";
import { Colors } from "react-native/Libraries/NewAppScreen";
import Client from "@/api/client";
interface AvatarProps extends TamaguiAvatarProps {
itemId: string;
@@ -11,8 +11,6 @@ interface AvatarProps extends TamaguiAvatarProps {
export default function Avatar(props: AvatarProps): React.JSX.Element {
const { server } = useApiClientContext();
return (
<YStack alignItems="center" marginHorizontal={10}>
<TamaguiAvatar
@@ -20,7 +18,7 @@ export default function Avatar(props: AvatarProps): React.JSX.Element {
borderRadius={!!!props.circular ? 4 : 'unset'}
{...props}
>
<TamaguiAvatar.Image src={`${server!.url}/Items/${props.itemId!}/Images/Primary`} />
<TamaguiAvatar.Image src={`${Client.server!.url}/Items/${props.itemId!}/Images/Primary`} />
<TamaguiAvatar.Fallback backgroundColor={Colors.Secondary}/>
</TamaguiAvatar>
{ props.children && (

View File

@@ -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 (
<Blurhash blurhash={props}>
</Blurhash>
)
interface BlurhashLoadingProps {
itemId: string;
blurhash: string;
size: number
}
export default BlurhashLoading;
export default function BlurhashLoading(props: BlurhashLoadingProps) : React.JSX.Element {
const { data: image, isSuccess } = useItemImage(Client.api!, props.itemId);
return (
<View minHeight={props.size}>
{ isSuccess ? (
<Image
src={image}
style={{
height: props.size,
width: props.size,
}}
/>
) : (
<Blurhash blurhash={props.blurhash} style={{ flex: 1 }} />
)
}
</View>
)
}

View File

@@ -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 (
<View
@@ -50,7 +48,7 @@ export function ItemCard(props: CardProps) {
</TamaguiCard.Header>
<TamaguiCard.Footer padded>
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
props.itemId,
ImageType.Logo,
@@ -70,7 +68,7 @@ export function ItemCard(props: CardProps) {
</TamaguiCard.Footer>
<TamaguiCard.Background>
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
props.itemId,
ImageType.Primary,

View File

@@ -1,7 +1,6 @@
import { useUserPlaylists } from "@/api/queries/playlist";
import { ItemCard } from "@/components/Global/helpers/item-card";
import { H2 } from "@/components/Global/helpers/text";
import { useApiClientContext } from "@/components/jellyfin-api-provider";
import { ProvidedHomeProps } from "@/components/types";
import React from "react";
import { FlatList } from "react-native";
@@ -9,9 +8,7 @@ import { View } from "tamagui";
export default function Playlists({ navigation }: ProvidedHomeProps) : React.JSX.Element {
const { apiClient, user, library } = useApiClientContext();
const { data: playlists } = useUserPlaylists(apiClient!, user!.id, library!.playlistLibraryId);
const { data: playlists } = useUserPlaylists();
return (
<View>

View File

@@ -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;

View File

@@ -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<boolean>(true);
const [serverAddress, setServerAddress] = useState<string | undefined>(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();
}
});

View File

@@ -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<string | undefined>('');
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 (
<SafeAreaView>
<H1>
{ `Sign in to ${server?.name ?? "Jellyfin"}`}
{ `Sign in to ${Client.server!.name ?? "Jellyfin"}`}
</H1>
<Button
onPress={() => {
setServer(undefined);
}}>
<Button onPress={() => Client.switchServer()}>
Switch Server
</Button>
@@ -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 });
}
}}

View File

@@ -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 (
<SafeAreaView>
@@ -59,19 +45,18 @@ export default function ServerLibrary(): React.JSX.Element {
<Button disabled={!!!libraryId}
onPress={() => {
setLibrary({
Client.setLibrary({
musicLibraryId: libraryId!,
musicLibraryName: libraries?.filter((library) => library.Id == libraryId)[0].Name ?? "No library name",
musicLibraryPrimaryImageId: libraries?.filter((library) => library.Id == libraryId)[0].ImageTags!.Primary,
playlistLibraryId: playlistLibrary!.Id!,
playlistLibraryPrimaryImageId: playlistLibrary!.ImageTags!.Primary,
})
});
}}>
Let's Go!
</Button>
<Button onPress={() => setUser(undefined)}>
<Button onPress={() => Client.switchUser()}>
Switch User
</Button>
</SafeAreaView>

View File

@@ -39,7 +39,7 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers<Para
alignContent="center"
flex={1}>
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
nowPlaying!.item.AlbumId ?? "",
ImageType.Primary,

View File

@@ -77,7 +77,7 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
}}
>
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
nowPlaying!.item.AlbumId ?? "",
ImageType.Primary,

View File

@@ -26,7 +26,7 @@ export default function Playlist(props: PlaylistProps): React.JSX.Element {
const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext();
const { data: tracks, isLoading, refetch } = useItemTracks(props.playlist.Id!, apiClient!);
const { data: tracks, isLoading, refetch } = useItemTracks(props.playlist.Id!);
useEffect(() => {
refetch();
@@ -39,7 +39,7 @@ export default function Playlist(props: PlaylistProps): React.JSX.Element {
<ScrollView contentInsetAdjustmentBehavior="automatic">
<YStack alignItems="center">
<CachedImage
source={getImageApi(apiClient!)
source={getImageApi(Client.api!)
.getItemImageUrlById(
props.playlist.Id!,
ImageType.Primary,

View File

@@ -14,14 +14,14 @@ export default function ServerDetails() : React.JSX.Element {
<H5>Access Token</H5>
<XStack>
<Icon name="hand-coin-outline" />
<Text>{apiClient!.accessToken}</Text>
<Text>{Client.api!!.accessToken}</Text>
</XStack>
</YStack>
<YStack>
<H5>Jellyfin Server</H5>
<XStack>
<Icon name="server-network" />
<Text>{apiClient!.basePath}</Text>
<Text>{Client.api!.basePath}</Text>
</XStack>
</YStack>
</YStack>

View File

@@ -8,7 +8,7 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack";
export default function Tracks({ navigation }: { navigation: NativeStackNavigationProp<StackParamList> }) : 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();

View File

@@ -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 (
<JellyfinApiClientProvider>
<PortalProvider shouldAddRootHost>
<App />
</PortalProvider>
</JellyfinApiClientProvider>
<PortalProvider shouldAddRootHost>
<App />
</PortalProvider>
);
}
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 (
<NavigationContainer theme={isDarkMode ? JellifyDarkTheme : JellifyLightTheme}>
<SafeAreaProvider>
{ server && library ? (
{ Client.user && Client.user ? (
<PlayerProvider>
<Navigation />
</PlayerProvider>

View File

@@ -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<SetStateAction<JellifyServer | undefined>>;
user: JellifyUser | undefined;
setUser: React.Dispatch<SetStateAction<JellifyUser | undefined>>;
library: JellifyLibrary | undefined;
setLibrary: React.Dispatch<SetStateAction<JellifyLibrary | undefined>>;
signOut: () => void
}
const JellyfinApiClientContextInitializer = () => {
const [apiClient, setApiClient] = useState<Api | undefined>(Client.instance.api);
const [sessionId, setSessionId] = useState<string>(Client.instance.sessionId);
const [user, setUser] = useState<JellifyUser | undefined>(Client.instance.user);
const [server, setServer] = useState<JellifyServer | undefined>(Client.instance.server);
const [library, setLibrary] = useState<JellifyLibrary | undefined>(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<JellyfinApiClientContext>({
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 (
<JellyfinApiClientContext.Provider value={{
apiClient,
sessionId,
server,
setServer,
user,
setUser,
library,
setLibrary,
signOut
}}>
{children}
</JellyfinApiClientContext.Provider>
);
};
export const useApiClientContext = () => useContext(JellyfinApiClientContext)

View File

@@ -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<boolean>(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);