Straightening out backend

Update ReadMe

Add Carplay and Async Storage dependencies
This commit is contained in:
Violet Caulfield
2024-10-02 09:34:44 -05:00
parent 6e8dfcf5f2
commit ce6160fe43
15 changed files with 202 additions and 87 deletions
+19 -6
View File
@@ -1,7 +1,15 @@
# Jellify
A music player for Jellyfin powered by React Native
A music player for Jellyfin powered by React Native. Designed to be lightweight, fast, and support ***extremely*** large music libraries (i.e., > 100K songs)
While other music players provide a WYSIWYG music player experience, *Jellify* caters to those who want a music player experience similar to what's provided by music streaming services. *Jellify* provides:
- Quick access to previously played and favorite tracks
- An infinitly<sup>TM</sup> customizable home screen
- Select from the provided quick filters or build a custom filter
- Support for Jellyfin mixes
- Quick access to similar artists and items for discovering music in your library
## Built with:
### Frontend
[React Native Reanimated](https://github.com/software-mansion/react-native-reanimated)
- [Marquee](https://github.com/animate-react-native/marquee)
@@ -11,10 +19,15 @@ A music player for Jellyfin powered by React Native
[React Native InApp Browser](https://github.com/proyecto26/react-native-inappbrowser)
### Backend
[Jellyfin SDK](https://typescript-sdk.jellyfin.org/)
[Tanstack Query](https://tanstack.com/query/latest/docs/framework/react/react-native)
[React Native Track Player](https://github.com/doublesymmetry/react-native-track-player)
[React Native Keychain](https://github.com/oblador/react-native-keychain)
[Jellyfin SDK](https://typescript-sdk.jellyfin.org/)\
[Tanstack Query](https://tanstack.com/query/latest/docs/framework/react/react-native)\
[React Native Track Player](https://github.com/doublesymmetry/react-native-track-player)\
[React Native Keychain](https://github.com/oblador/react-native-keychain)\
[React Native Async Storage](https://github.com/react-native-async-storage/async-storage)
### Logging
[GlitchTip](https://glitchtip.com/) captures anonymous logging if and only if the user opts into it. This can be toggled at anytime
[GlitchTip](https://glitchtip.com/)
- Captures anonymous logging if and only if the user opts into it. This can be toggled at anytime
### Passion
Love from [me](https://github.com/anultravioletaurora)! I've learned a lot from working on this project (and the many failed attempts before it) and I hope you enjoy using it!
+3
View File
@@ -0,0 +1,3 @@
export enum AsyncStorageKeys {
ServerUrl = "SERVER_URL"
}
+5
View File
@@ -0,0 +1,5 @@
export enum MutationKeys {
AccessToken = "ACCESS_TOKEN",
Credentials = "CREDENTIALS",
ServerUrl = "SERVER_URL"
}
+6
View File
@@ -0,0 +1,6 @@
export enum QueryKeys {
Api = "API",
ArtistById = "ARTIST_BY_ID",
Credentials = "CREDENTIALS",
ServerUrl = "SERVER_URL"
}
+36
View File
@@ -0,0 +1,36 @@
import { useMutation } from "@tanstack/react-query";
import { MutationKeys } from "../enums/mutation-keys";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { AsyncStorageKeys } from "../enums/async-storage-keys";
import * as Keychain from "react-native-keychain"
import { useServerUrl } from "../queries/storage";
import { Jellyfin } from "@jellyfin/sdk";
import { client } from "../queries";
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api"
import { JellyfinCredentials } from "../types/jellyfin-credentials";
export const serverUrl = useMutation({
mutationKey: [MutationKeys.ServerUrl],
mutationFn: (serverUrl: string) => {
let jellyfin = new Jellyfin(client);
let api = jellyfin.createApi(serverUrl);
return getSystemApi(api).getPublicSystemInfo()
},
onSuccess: (publicSystemInfoResponse, serverUrl, context) => {
if (!!!publicSystemInfoResponse.data.Version)
throw new Error("Unable to connect to Jellyfin Server");
return AsyncStorage.setItem(AsyncStorageKeys.ServerUrl, serverUrl);
}
});
export const credentials = useMutation({
mutationKey: [MutationKeys.Credentials],
mutationFn: (credentials: JellyfinCredentials) => {
return Keychain.setInternetCredentials(useServerUrl.data!, credentials.username, credentials.accessToken);
},
});
+29
View File
@@ -0,0 +1,29 @@
import { Jellyfin } from "@jellyfin/sdk"
import { useQuery } from "@tanstack/react-query";
import { getDeviceNameSync, getUniqueIdSync } from "react-native-device-info"
import { QueryKeys } from "./enums/query-keys";
import { useServerUrl } from "./queries/storage";
import { useCredentials } from "./queries/keychain";
import { SharedWebCredentials } from "react-native-keychain";
let clientName : string = require('root-require')('./package.json').name
let clientVersion : string = require('root-require')('./package.json').version
export const client : Jellyfin = new Jellyfin({
clientInfo: {
name: clientName,
version: clientVersion
},
deviceInfo: {
name: getDeviceNameSync(),
id: getUniqueIdSync()
}
});
export const useApi = useQuery({
queryKey: [QueryKeys.Api],
queryFn: () => {
let credentials = useCredentials.data!
return client.createApi(credentials.server, credentials.password);
}
})
-14
View File
@@ -1,14 +0,0 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
import { useQuery, UseQueryResult } from '@tanstack/react-query'
import { JellyfinService } from '../services/jellyfin-service'
import { ArtistModel } from '../../models/ArtistModel'
namespace ArtistQueries {
export function fetchArtistById(artistJellyfinId: string) : UseQueryResult<ArtistModel> {
return useQuery({
queryKey: ['artists', artistJellyfinId],
queryFn: ({ queryKey }) => JellyfinService.instance.getArtistById(queryKey[1])
})
}
}
+14
View File
@@ -0,0 +1,14 @@
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/items-api"
import { useApi } from "../queries";
export const useArtistById = (artistId: string) => useQuery({
queryKey: [QueryKeys.ArtistById, artistId],
queryFn: (({ queryKey }) => {
return getItemsApi(useApi.data!).getItems({ ids: [queryKey[1]]});
})
})
-4
View File
@@ -1,4 +0,0 @@
namespace ImageQueries {
}
+21
View File
@@ -0,0 +1,21 @@
import { UseQueryResult, useQuery } from "@tanstack/react-query"
import * as Keychain from "react-native-keychain"
import { ArtistModel } from "../../models/ArtistModel"
import { Api } from "@jellyfin/sdk"
import AsyncStorage from "@react-native-async-storage/async-storage"
import { AsyncStorageKeys } from "../enums/async-storage-keys"
import { QueryKeys } from "../enums/query-keys"
import { useServerUrl } from "./storage"
export const useCredentials = useQuery({
queryKey: [QueryKeys.Credentials],
queryFn: () => {
return Keychain.getInternetCredentials(useServerUrl.data!)
.then((keychain) => {
if (!keychain)
throw new Error("Jellyfin server credentials not stored in keychain");
return keychain as Keychain.SharedWebCredentials
});
}
});
+12
View File
@@ -0,0 +1,12 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { UseQueryResult, useQuery } from "@tanstack/react-query";
import { AsyncStorageKeys } from "../enums/async-storage-keys";
import { QueryKeys } from "../enums/query-keys";
export const useServerUrl: UseQueryResult<string> = useQuery({
queryKey: [QueryKeys.ServerUrl],
queryFn: (() => {
return AsyncStorage.getItem(AsyncStorageKeys.ServerUrl);
})
});
-63
View File
@@ -1,63 +0,0 @@
import { Api, Jellyfin } from "@jellyfin/sdk";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api"
import { getDeviceNameSync, getUniqueIdSync } from "react-native-device-info";
import { ArtistModel } from "../../models/ArtistModel";
import * as Keychain from "react-native-keychain"
let clientName : string = require('root-require')('./package.json').name
let clientVersion : string = require('root-require')('./package.json').version
export class JellyfinService {
private static _instance: JellyfinService;
private api?: Api;
// TODO: This should read in auth details when constructed?
private constructor() {};
private client : Jellyfin = new Jellyfin({
clientInfo: {
name: clientName,
version: clientVersion
},
deviceInfo: {
name: getDeviceNameSync(),
id: getUniqueIdSync()
}
})
public static get instance() {
// TODO: Determine this makes singleton correctly
return this._instance || (this._instance = new this())
}
public static get api() : Api {
return this.instance.api!
}
public initConnection(serverUrl: string) : void {
JellyfinService.instance.api = JellyfinService.instance.client.createApi(serverUrl);
}
public storeAuthDetails(serverUrl: string, username: string, sessionToken: string) : Promise<false | Keychain.Result> {
// Set sessionToken in API
JellyfinService.instance.api = JellyfinService.instance.client.createApi(serverUrl, sessionToken);
// Store in encrypted local storage
return Keychain.setInternetCredentials(serverUrl, username, sessionToken)
// TODO: Refresh on reopen?
}
public async getArtistById(artistJellyfinId: string) : Promise<ArtistModel> {
return getItemsApi(JellyfinService.instance.api!)
.getItems({ ids: [artistJellyfinId]})
// TODO: Error handling here
.then((response) => response.data.Items!.at(0)!)
.then(item => new ArtistModel(item))
}
}
+10
View File
@@ -0,0 +1,10 @@
export class JellyfinCredentials {
username: string;
accessToken: string;
constructor(username: string, accessToken: string) {
this.username = username;
this.accessToken = accessToken;
}
}
+45
View File
@@ -9,7 +9,9 @@
"version": "0.0.1",
"dependencies": {
"@animatereactnative/marquee": "^0.2.0",
"@gcores/react-native-carplay": "^1.1.12",
"@jellyfin/sdk": "^0.10.0",
"@react-native-async-storage/async-storage": "^2.0.0",
"@sentry/react-native": "^5.30.0",
"@tanstack/react-query": "^5.52.1",
"react": "18.3.1",
@@ -2244,6 +2246,16 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@gcores/react-native-carplay": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@gcores/react-native-carplay/-/react-native-carplay-1.1.12.tgz",
"integrity": "sha512-JavYywgXAA/XOtuW/P5fGuw+PwJC4qDvMYVRMHiIF8pJWbxl3zihautfQZzsU94ApFMoVdOPNhjHzhNXn5a8hg==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -3173,6 +3185,18 @@
"node": ">= 8"
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.0.0.tgz",
"integrity": "sha512-af6H9JjfL6G/PktBfUivvexoiFKQTJGQCtSWxMdivLzNIY94mu9DdiY0JqCSg/LyPCLGKhHPUlRQhNvpu3/KVA==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native-community/cli": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-14.0.0.tgz",
@@ -9477,6 +9501,15 @@
"node": ">=8"
}
},
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -12082,6 +12115,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+2
View File
@@ -11,7 +11,9 @@
},
"dependencies": {
"@animatereactnative/marquee": "^0.2.0",
"@gcores/react-native-carplay": "^1.1.12",
"@jellyfin/sdk": "^0.10.0",
"@react-native-async-storage/async-storage": "^2.0.0",
"@sentry/react-native": "^5.30.0",
"@tanstack/react-query": "^5.52.1",
"react": "18.3.1",