mirror of
https://github.com/Jellify-Music/App.git
synced 2026-05-12 14:28:46 -05:00
Straightening out backend
Update ReadMe Add Carplay and Async Storage dependencies
This commit is contained in:
@@ -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!
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum AsyncStorageKeys {
|
||||
ServerUrl = "SERVER_URL"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum MutationKeys {
|
||||
AccessToken = "ACCESS_TOKEN",
|
||||
Credentials = "CREDENTIALS",
|
||||
ServerUrl = "SERVER_URL"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum QueryKeys {
|
||||
Api = "API",
|
||||
ArtistById = "ARTIST_BY_ID",
|
||||
Credentials = "CREDENTIALS",
|
||||
ServerUrl = "SERVER_URL"
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
@@ -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])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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]]});
|
||||
})
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
namespace ImageQueries {
|
||||
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
})
|
||||
});
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
export class JellyfinCredentials {
|
||||
username: string;
|
||||
accessToken: string;
|
||||
|
||||
constructor(username: string, accessToken: string) {
|
||||
this.username = username;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
||||
Generated
+45
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user