mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2026-05-12 16:39:10 -05:00
feat: Discover CarPlay Template (#1159)
* feat: Discover CarPlay Template * fix quick actions maestro * make me a recording in maestro plz tho * update docs, maestro quick action flows * update favoriting maestro flow * maestro quick action flow fixes * add recently added albums to discover on carplay * search flow reset stuff * adjust gd search maestro subflow * we can't test the player after we've signed out lol * run player flow eariler in full flow * Update queue.yaml
This commit is contained in:
+92
-31
@@ -1,22 +1,27 @@
|
||||
# Contributing
|
||||
|
||||
We are open to any developer that wants to lend their hand at _Jellify_ development, and developers can join our [Discord server](https://discord.gg/jellify) to get in contact with us.
|
||||
## Table of Contents
|
||||
|
||||
Here's the best way to get started:
|
||||
- [Getting Started](#getting-started)
|
||||
- [Running Locally](#️running-locally)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Code Style](#code-style)
|
||||
- [Testing](#testing)
|
||||
- [Submitting a Pull Request](#submitting-a-pull-request)
|
||||
|
||||
- Fork this repository
|
||||
- Follow the instructions for [Running Locally](#️running-locally)
|
||||
- Check out the [issues](https://github.com/Jellify-Music/App/issues) if you need inspiration
|
||||
- Hack, hack, hack
|
||||
- ???
|
||||
- Submit a Pull Request to sync the main repository with your fork
|
||||
- Profit! 🎉
|
||||
## Getting Started
|
||||
|
||||
1. Fork this repository
|
||||
2. Follow the instructions for [Running Locally](#️running-locally)
|
||||
3. Check out the [issues](https://github.com/Jellify-Music/App/issues) if you need inspiration
|
||||
4. Hack, hack, hack
|
||||
5. Submit a Pull Request to sync the main repository with your fork
|
||||
|
||||
## Running Locally
|
||||
|
||||
### Universal Dependencies
|
||||
|
||||
- [NodeJS v22](https://nodejs.org/en/download) for React Native
|
||||
- [Node.js v22](https://nodejs.org/en/download)
|
||||
- [Bun](https://bun.sh/) for managing dependencies
|
||||
|
||||
### 🍎 iOS
|
||||
@@ -25,28 +30,26 @@ Here's the best way to get started:
|
||||
|
||||
- [Xcode](https://developer.apple.com/xcode/) for building
|
||||
|
||||
#### Instructions
|
||||
|
||||
##### Setup
|
||||
#### Setup
|
||||
|
||||
- Clone this repository
|
||||
- Run `bun init-ios` to initialize the project
|
||||
- This will install `npm` packages, install `bundler` and required gems, and install required CocoaPods with [React Native's New Architecture](https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here#what-is-the-new-architecture)
|
||||
|
||||
##### Running
|
||||
#### Running
|
||||
|
||||
- Run `bun start` to start the dev server
|
||||
- Open the `Jellify.xcodeworkspace` with Xcode, _not_ the `Jellify.xcodeproject`
|
||||
- Run `bun start` to start the Metro dev server
|
||||
- Open `Jellify.xcworkspace` with Xcode, _not_ `Jellify.xcodeproj`
|
||||
- Run in the simulator
|
||||
- _You will need to wait for Xcode to finish it's "Indexing" step_
|
||||
- _You will need to wait for Xcode to finish its "Indexing" step before the build completes_
|
||||
|
||||
- To run on device, you will need access to the *Signing* Repository
|
||||
- Setup a GitHub Personal Access Token and export it to a variable "MATCH_REPO_PAT"
|
||||
- To run on a physical device, you will need access to the _Signing_ repository
|
||||
- Create a GitHub Personal Access Token and export it as `MATCH_REPO_PAT`
|
||||
- Run `bun fastlane:ios:match` to fetch the signing keys and certificates
|
||||
|
||||
##### Building
|
||||
#### Building
|
||||
|
||||
- To create a build, run `bun fastlane:ios:build` to use fastlane to compile an `.ipa`
|
||||
- Run `bun fastlane:ios:build` to use Fastlane to compile an `.ipa`
|
||||
|
||||
### 🤖 Android
|
||||
|
||||
@@ -55,27 +58,85 @@ Here's the best way to get started:
|
||||
- [Android Studio](https://developer.android.com/studio)
|
||||
- [Java Development Kit](https://www.oracle.com/th/java/technologies/downloads/)
|
||||
|
||||
#### Instructions
|
||||
|
||||
##### Setup
|
||||
#### Setup
|
||||
|
||||
- Clone this repository
|
||||
- Run `bun install` to install `npm` packages
|
||||
|
||||
##### Running
|
||||
#### Running
|
||||
|
||||
- Run `bun start` to start the dev server
|
||||
- Run `bun start` to start the Metro dev server
|
||||
- Open the `android` folder with Android Studio
|
||||
- _Android Studio should automatically grab the "Run Configurations" and initialize Gradle_
|
||||
- Run either on a device or in the simulator
|
||||
- _Android Studio should automatically detect the run configurations and initialize Gradle_
|
||||
- Run on a device or in the emulator
|
||||
|
||||
##### Building
|
||||
#### Building
|
||||
|
||||
- To create a build, run `bun fastlane:android:build` to use fastlane to compile an `.apk` for all architectures
|
||||
- Alternatively, run `cd android; ./gradlew assembleRelease` to use Gradle to compile an `.apk`
|
||||
- Run `bun fastlane:android:build` to use Fastlane to compile an `.apk` for all architectures
|
||||
- Alternatively, run `cd android && ./gradlew assembleRelease` to use Gradle directly
|
||||
|
||||
#### References
|
||||
|
||||
- [Setting up Android SDK](https://developer.android.com/about/versions/14/setup-sdk)
|
||||
- [ANDROID_HOME not being set](https://stackoverflow.com/questions/26356359/error-android-home-is-not-set-and-android-command-not-in-your-path-you-must/54888107#54888107)
|
||||
- [Android Auto app not showing up](https://www.reddit.com/r/AndroidAuto/s/LGYHoSPdXm)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
api/ # Jellyfin API calls and helpers
|
||||
components/ # Shared UI components
|
||||
configs/ # App configuration (Tamagui, etc.)
|
||||
constants/ # App-wide constants
|
||||
enums/ # TypeScript enums
|
||||
hooks/ # Custom React hooks
|
||||
providers/ # React context providers
|
||||
screens/ # Top-level screen components
|
||||
services/ # Background services (e.g. player)
|
||||
stores/ # State management stores
|
||||
types/ # TypeScript types and interfaces
|
||||
utils/ # Utility / helper functions
|
||||
jest/
|
||||
contextual/ # Component and integration tests
|
||||
functional/ # Unit tests for utilities and logic
|
||||
setup/ # Jest setup and mock files
|
||||
maestro/
|
||||
flows/ # E2E UI test flows grouped by feature area
|
||||
subflows/ # Reusable flows shared across multiple stacks
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
The project uses ESLint, Prettier, and TypeScript for code quality. Before submitting a PR, please ensure your changes pass all checks:
|
||||
|
||||
```sh
|
||||
bun lint # Check for ESLint errors
|
||||
bun format:check # Check Prettier formatting
|
||||
bun format # Auto-fix Prettier formatting
|
||||
bun tsc # Type-check with TypeScript
|
||||
```
|
||||
|
||||
Notable style rules enforced by ESLint:
|
||||
- No semicolons
|
||||
- `@typescript-eslint/no-explicit-any` is an **error** — avoid `any` types
|
||||
- `react/react-in-jsx-scope` is disabled (React 17+ JSX transform)
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are written with [Jest](https://jestjs.io/) and [React Native Testing Library](https://callstack.github.io/react-native-testing-library/).
|
||||
|
||||
```sh
|
||||
bun test # Run the full test suite
|
||||
```
|
||||
|
||||
Tests live in `jest/contextual/` (component / integration tests) and `jest/functional/` (unit tests). When adding new functionality, please include relevant tests.
|
||||
|
||||
Jellify also has an end-to-end UI test suite powered by [Maestro](https://maestro.mobile.dev). See the [Maestro README](maestro/README.md) for details on the flow structure and how to run the tests.
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
- PRs are submitted against the `main` branch
|
||||
- Fill out the [pull request template](.github/pull_request_template.md) — include a clear description of what changed and what issue it addresses
|
||||
- Tag `@anultravioletaurora` as a reviewer
|
||||
- CI will automatically run the Jest test suite, TypeScript type-check, and ESLint on your PR — make sure all checks pass before requesting review
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
appId: com.cosmonautical.jellify
|
||||
---
|
||||
# Full test suite flow - runs all tests in order
|
||||
- startRecording: full_test_suite
|
||||
|
||||
# Run setup and login to Jellyfin
|
||||
# State is always cleared in the setup flow
|
||||
@@ -9,6 +10,9 @@ appId: com.cosmonautical.jellify
|
||||
# Home screen operations
|
||||
- runFlow: flows/home/flow.yaml
|
||||
|
||||
# Full-screen player (requires a track to be playing from a prior flow)
|
||||
- runFlow: flows/player/flow.yaml
|
||||
|
||||
# Run the Quick Actions flow from the home screen
|
||||
- runFlow: flows/quick-actions/flow.yaml
|
||||
|
||||
@@ -21,8 +25,7 @@ appId: com.cosmonautical.jellify
|
||||
# Discover tab
|
||||
- runFlow: flows/discover/flow.yaml
|
||||
|
||||
# Settings tab
|
||||
# Settings tab - and sign out at the end to complete the test run
|
||||
- runFlow: flows/settings/flow.yaml
|
||||
|
||||
# Full-screen player (requires a track to be playing from a prior flow)
|
||||
- runFlow: flows/player/flow.yaml
|
||||
- stopRecording
|
||||
|
||||
@@ -5,7 +5,7 @@ appId: com.cosmonautical.jellify
|
||||
- takeScreenshot: screenshots/player/queue_screen_initial_state
|
||||
|
||||
- assertVisible:
|
||||
id: "queue-item-0"
|
||||
id: "queue-item-1"
|
||||
|
||||
# Try scrolling in queue
|
||||
- scrollUntilVisible:
|
||||
|
||||
@@ -14,14 +14,14 @@ appId: com.cosmonautical.jellify
|
||||
- tapOn:
|
||||
id: "library-tracks-tab-button"
|
||||
|
||||
# Wait for tracks to load
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "track-item-0"
|
||||
timeout: 30000
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: "track-item-1"
|
||||
direction: UP
|
||||
timeout: 60000
|
||||
|
||||
# Take screenshot of tracks list
|
||||
- takeScreenshot: screenshots/40_tracks_before_favorite
|
||||
- takeScreenshot: screenshots/quick_actions/tracks_list_screen
|
||||
|
||||
# Swipe right on first track to reveal quick actions (may include favorite toggle)
|
||||
- swipe:
|
||||
@@ -33,7 +33,7 @@ appId: com.cosmonautical.jellify
|
||||
- waitForAnimationToEnd
|
||||
|
||||
# Take screenshot showing quick action buttons
|
||||
- takeScreenshot: screenshots/41_swipe_actions
|
||||
- takeScreenshot: screenshots/quick_actions/swipe_actions
|
||||
|
||||
# Check if quick action buttons appeared
|
||||
- assertVisible:
|
||||
@@ -49,7 +49,7 @@ appId: com.cosmonautical.jellify
|
||||
- waitForAnimationToEnd
|
||||
|
||||
# Take screenshot after action
|
||||
- takeScreenshot: screenshots/42_after_favorite_action
|
||||
- takeScreenshot: screenshots/quick_actions/after_favorite_action
|
||||
|
||||
# Test swipe in the other direction
|
||||
- swipe:
|
||||
@@ -60,7 +60,7 @@ appId: com.cosmonautical.jellify
|
||||
- waitForAnimationToEnd
|
||||
|
||||
# Take screenshot of right swipe actions
|
||||
- takeScreenshot: screenshots/43_right_swipe_actions
|
||||
- takeScreenshot: screenshots/quick_actions/right_swipe_actions
|
||||
|
||||
# Tap quick action if visible
|
||||
- tapOn:
|
||||
|
||||
@@ -150,7 +150,7 @@ appId: com.cosmonautical.jellify
|
||||
- tapOn: "Tracks"
|
||||
|
||||
# Wait for tracks to load
|
||||
- assertVisible: "Tracks"
|
||||
- waitForAnimationToEnd
|
||||
|
||||
# Scroll to ensure we're not at the top
|
||||
- scroll
|
||||
@@ -183,7 +183,7 @@ appId: com.cosmonautical.jellify
|
||||
id: "search-tab-button"
|
||||
|
||||
# Wait for search screen
|
||||
- assertVisible: "Search"
|
||||
- waitForAnimationToEnd
|
||||
|
||||
# Type a search query
|
||||
- tapOn:
|
||||
|
||||
@@ -11,4 +11,4 @@ appId: com.cosmonautical.jellify
|
||||
- takeScreenshot: screenshots/search/search_initial_state_screen
|
||||
|
||||
# Test Grateful Dead lookup
|
||||
- runFlow: "gd-search.yaml"
|
||||
- runFlow: "gd-search.yaml"
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
appId: com.cosmonautical.jellify
|
||||
---
|
||||
|
||||
- assertVisible:
|
||||
id: "search-input"
|
||||
# Quick actions may have brought up some search results already, so scroll to the top of the search tab to ensure we're in a consistent state
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
id: "search-input"
|
||||
direction: UP
|
||||
timeout: 180000
|
||||
|
||||
# Tap on the search input to focus it and bring up the keyboard
|
||||
- tapOn:
|
||||
id: "search-input"
|
||||
|
||||
|
||||
# Erase any existing text in the search input
|
||||
- eraseText
|
||||
|
||||
# Search for the Grateful Dead
|
||||
- inputText: "Grateful Dead"
|
||||
|
||||
# Hide the keyboard to reveal search results
|
||||
- hideKeyboard
|
||||
|
||||
# Wait for results to load
|
||||
@@ -16,6 +27,7 @@ appId: com.cosmonautical.jellify
|
||||
# Take screenshot of search results
|
||||
- takeScreenshot: screenshots/search/search_results_screen
|
||||
|
||||
# Ensure the Grateful Dead artist result is visible and tap on it
|
||||
- assertVisible:
|
||||
id: "artist-search-result-0"
|
||||
- tapOn:
|
||||
@@ -24,4 +36,5 @@ appId: com.cosmonautical.jellify
|
||||
# Take screenshot of artist from search
|
||||
- takeScreenshot: screenshots/search/artist_from_search_screen
|
||||
|
||||
- runFlow: ../../subflows/artist/flow.yaml
|
||||
# Run the artist subflow to verify we can navigate to the artist page from search results and that the page loads correctly
|
||||
- runFlow: ../../subflows/artist/flow.yaml
|
||||
|
||||
@@ -12,14 +12,13 @@ import { RefObject, useRef } from 'react'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { fetchRecentlyAdded } from '../recents/utils'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
import { fetchAlbumDiscs } from '../item'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import { AlbumDiscsQueryKey } from './keys'
|
||||
import { AlbumQuery } from './queries'
|
||||
import { AlbumQuery, RecentlyAddedQuery } from './queries'
|
||||
|
||||
export const useAlbum = (album: BaseItemDto) => useQuery(AlbumQuery(album))
|
||||
|
||||
@@ -110,16 +109,11 @@ export default useAlbums
|
||||
|
||||
export const useRecentlyAddedAlbums = () => {
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
initialPageParam: 0,
|
||||
})
|
||||
return useInfiniteQuery(RecentlyAddedQuery(api, user, library))
|
||||
}
|
||||
|
||||
export const useRefetchRecentlyAdded: () => () => void = () => {
|
||||
@@ -137,7 +131,10 @@ export const useAlbumDiscs = (album: BaseItemDto) => {
|
||||
return useQuery(AlbumDiscsQuery(api, album))
|
||||
}
|
||||
|
||||
export const AlbumDiscsQuery = (api: Api | undefined, album: BaseItemDto) => ({
|
||||
export const ensureAlbumDiscsQuery = async (album: BaseItemDto) =>
|
||||
await queryClient.ensureQueryData(AlbumDiscsQuery(getApi(), album))
|
||||
|
||||
const AlbumDiscsQuery = (api: Api | undefined, album: BaseItemDto) => ({
|
||||
queryKey: AlbumDiscsQueryKey(album),
|
||||
queryFn: () => fetchAlbumDiscs(api, album),
|
||||
})
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ONE_DAY } from '../../../constants/query-client'
|
||||
import { getApi } from '../../../stores'
|
||||
import { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
|
||||
import { ONE_DAY, queryClient } from '../../../constants/query-client'
|
||||
import { getApi, getLibrary, getUser } from '../../../stores'
|
||||
import { fetchItem } from '../item'
|
||||
import { RecentlyAddedQueryKey } from '../recents/keys'
|
||||
import { fetchRecentlyAdded } from '../recents/utils'
|
||||
import { AlbumQueryKey } from './keys'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
|
||||
export const AlbumQuery = (album: BaseItemDto) => {
|
||||
const api = getApi()
|
||||
@@ -14,3 +18,28 @@ export const AlbumQuery = (album: BaseItemDto) => {
|
||||
staleTime: ONE_DAY,
|
||||
}
|
||||
}
|
||||
|
||||
export const RecentlyAddedQuery = (
|
||||
api: Api | undefined = getApi(),
|
||||
user = getUser(),
|
||||
library = getLibrary(),
|
||||
) => {
|
||||
return {
|
||||
queryKey: RecentlyAddedQueryKey(user, library),
|
||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
initialPageParam: 0,
|
||||
} as UndefinedInitialDataInfiniteOptions<
|
||||
BaseItemDto[],
|
||||
Error,
|
||||
BaseItemDto[],
|
||||
(string | undefined)[],
|
||||
number
|
||||
>
|
||||
}
|
||||
|
||||
export const ensureRecentlyAddedQueryData = async () => {
|
||||
return queryClient.ensureInfiniteQueryData(RecentlyAddedQuery())
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { fetchArtistAlbums, fetchArtistFeaturedOn, fetchArtists } from './utils/artist'
|
||||
import { fetchArtistFeaturedOn, fetchArtists } from './utils/artist'
|
||||
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { RefObject, useRef } from 'react'
|
||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||
import { getApi, useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||
import { getApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
import { fetchItem } from '../item'
|
||||
import { ArtistQueryKey } from './keys'
|
||||
import { artistAlbumsQuery } from './queries'
|
||||
|
||||
export const useArtist = (artistId: string | undefined | null) => {
|
||||
const api = getApi()
|
||||
@@ -27,23 +28,17 @@ export const useArtist = (artistId: string | undefined | null) => {
|
||||
}
|
||||
|
||||
export const useArtistAlbums = (artist: BaseItemDto) => {
|
||||
const api = useApi()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QueryKeys.ArtistAlbums, library?.musicLibraryId, artist.Id],
|
||||
queryFn: () => fetchArtistAlbums(api, library?.musicLibraryId, artist),
|
||||
enabled: !isUndefined(artist.Id),
|
||||
})
|
||||
return useQuery(artistAlbumsQuery(library!, artist))
|
||||
}
|
||||
|
||||
export const useArtistFeaturedOn = (artist: BaseItemDto) => {
|
||||
const api = useApi()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QueryKeys.ArtistFeaturedOn, library?.musicLibraryId, artist.Id],
|
||||
queryFn: () => fetchArtistFeaturedOn(api, library?.musicLibraryId, artist),
|
||||
queryFn: () => fetchArtistFeaturedOn(library?.musicLibraryId, artist),
|
||||
enabled: !isUndefined(artist.Id),
|
||||
})
|
||||
}
|
||||
@@ -52,7 +47,6 @@ export const useAlbumArtists: () => [
|
||||
RefObject<Set<string>>,
|
||||
UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>,
|
||||
] = () => {
|
||||
const api = useApi()
|
||||
const [user] = useJellifyUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
@@ -93,7 +87,6 @@ export const useAlbumArtists: () => [
|
||||
],
|
||||
queryFn: ({ pageParam }: { pageParam: number }) =>
|
||||
fetchArtists(
|
||||
api,
|
||||
user,
|
||||
library,
|
||||
pageParam,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
enum ArtistQueryKeys {
|
||||
ArtistById = 'ARTIST_BY_ID',
|
||||
ArtistAlbums = 'ARTIST_ALBUMS',
|
||||
}
|
||||
|
||||
export const ArtistQueryKey = (artistId: string | undefined | null) => [
|
||||
QueryKeys.ArtistById,
|
||||
ArtistQueryKeys.ArtistById,
|
||||
artistId,
|
||||
]
|
||||
|
||||
export const ArtistAlbumsQueryKey = (artistId: string | undefined | null) => [
|
||||
ArtistQueryKeys.ArtistAlbums,
|
||||
artistId,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { ArtistAlbumsQueryKey } from './keys'
|
||||
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
|
||||
import { fetchArtistAlbums } from './utils/artist'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { getLibrary } from '../../../stores'
|
||||
|
||||
export const artistAlbumsQuery = (library: JellifyLibrary, artist: BaseItemDto) => ({
|
||||
queryKey: ArtistAlbumsQueryKey(artist.Id),
|
||||
queryFn: () => fetchArtistAlbums(library?.musicLibraryId, artist),
|
||||
enabled: !isUndefined(artist.Id),
|
||||
})
|
||||
|
||||
export async function ensureArtistAlbumsQueryData(artist: BaseItemDto) {
|
||||
const library = getLibrary()
|
||||
return await queryClient.ensureQueryData(artistAlbumsQuery(library!, artist))
|
||||
}
|
||||
@@ -12,9 +12,9 @@ import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import { ApiLimits } from '../../../../configs/query.config'
|
||||
import { setQueryUserDataForItems } from '../../user-data'
|
||||
import { getApi } from '../../../../stores'
|
||||
|
||||
export function fetchArtists(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
page: number,
|
||||
@@ -23,6 +23,8 @@ export function fetchArtists(
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (!api) return reject('No API instance provided')
|
||||
if (!user) return reject('No user provided')
|
||||
if (!library) return reject('Library has not been set')
|
||||
@@ -55,16 +57,17 @@ export function fetchArtists(
|
||||
|
||||
/**
|
||||
* Fetches all albums for an artist
|
||||
* @param api The Jellyfin {@link Api} instance
|
||||
* @param libraryId The ID of the library to fetch albums from
|
||||
* @param artist The artist to fetch albums for
|
||||
* @returns A promise that resolves to an array of {@link BaseItemDto}s
|
||||
*/
|
||||
export function fetchArtistAlbums(
|
||||
api: Api | undefined,
|
||||
libraryId: string | undefined,
|
||||
artist: BaseItemDto,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (!api) return reject('No API instance provided')
|
||||
if (!libraryId) return reject('Library has not been set')
|
||||
|
||||
@@ -98,11 +101,12 @@ export function fetchArtistAlbums(
|
||||
* @returns A promise that resolves to an array of {@link BaseItemDto}s
|
||||
*/
|
||||
export function fetchArtistFeaturedOn(
|
||||
api: Api | undefined,
|
||||
libraryId: string | undefined,
|
||||
artist: BaseItemDto,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (!api) return reject('No API instance provided')
|
||||
if (!libraryId) return reject('Library has not been set')
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RecentlyPlayedArtistsQueryKey, RecentlyPlayedTracksQueryKey } from './keys'
|
||||
import { InfiniteData, useInfiniteQuery, useQueries } from '@tanstack/react-query'
|
||||
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from './utils'
|
||||
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
@@ -22,3 +22,8 @@ export const RecentlyPlayedArtistsQueryKey = (
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
) => RecentsQueryKey(RecentsQueryKeys.RecentlyPlayedArtists, user, library)
|
||||
|
||||
export const RecentlyAddedQueryKey = (
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
) => RecentsQueryKey(RecentsQueryKeys.RecentlyAdded, user, library)
|
||||
|
||||
@@ -1,83 +1,50 @@
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
|
||||
import { SuggestionQueryKeys } from './keys'
|
||||
import {
|
||||
fetchAlbumSuggestions,
|
||||
fetchArtistSuggestions,
|
||||
fetchSearchSuggestions,
|
||||
} from './utils/suggestions'
|
||||
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
|
||||
import { fetchSearchSuggestions } from './utils/suggestions'
|
||||
import { getUser, useJellifyLibrary } from '../../../stores'
|
||||
import { isUndefined } from 'lodash'
|
||||
import fetchSimilarArtists, { fetchSimilarItems } from './utils/similar'
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { ONE_DAY } from '../../../constants/query-client'
|
||||
import { DiscoverAlbumsQuery, DiscoverArtistsQuery } from './queries'
|
||||
|
||||
export const useSearchSuggestions = () => {
|
||||
const api = getApi()
|
||||
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const user = getUser()
|
||||
|
||||
return useQuery({
|
||||
queryKey: [SuggestionQueryKeys.SearchSuggestions, library?.musicLibraryId],
|
||||
queryFn: () => fetchSearchSuggestions(api, user, library?.musicLibraryId),
|
||||
queryFn: () => fetchSearchSuggestions(user, library?.musicLibraryId),
|
||||
enabled: !isUndefined(library),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDiscoverArtists = () => {
|
||||
const api = getApi()
|
||||
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const user = getUser()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
SuggestionQueryKeys.InfiniteArtistSuggestions,
|
||||
user?.id,
|
||||
library?.musicLibraryId,
|
||||
],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchArtistSuggestions(api, user, library?.musicLibraryId, pageParam),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
maxPages: 2,
|
||||
})
|
||||
return useInfiniteQuery(DiscoverArtistsQuery(user, library))
|
||||
}
|
||||
|
||||
export const useDiscoverAlbums = () => {
|
||||
const api = getApi()
|
||||
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const user = getUser()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: [SuggestionQueryKeys.InfiniteAlbumSuggestions, user?.id, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchAlbumSuggestions(api, user, library?.musicLibraryId, pageParam),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
maxPages: 2,
|
||||
})
|
||||
return useInfiniteQuery(DiscoverAlbumsQuery(user, library))
|
||||
}
|
||||
|
||||
export const useSimilarItems = (item: BaseItemDto) => {
|
||||
const api = getApi()
|
||||
|
||||
const user = getUser()
|
||||
|
||||
return useQuery({
|
||||
queryKey: [SuggestionQueryKeys.SimilarItems, item.Id],
|
||||
queryFn: () =>
|
||||
item.Type === BaseItemKind.MusicArtist
|
||||
? fetchSimilarArtists(api, user, item.Id!)
|
||||
: fetchSimilarItems(api, user, item.Id!),
|
||||
? fetchSimilarArtists(user, item.Id!)
|
||||
: fetchSimilarItems(user, item.Id!),
|
||||
enabled: !isUndefined(item.Id),
|
||||
staleTime: ONE_DAY,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum SuggestionQueryKeys {
|
||||
InfiniteArtistSuggestions,
|
||||
SearchSuggestions,
|
||||
SimilarItems,
|
||||
InfiniteAlbumSuggestions,
|
||||
InfiniteArtistSuggestions = 'INFINITE_ARTIST_SUGGESTIONS',
|
||||
SearchSuggestions = 'SEARCH_SUGGESTIONS',
|
||||
SimilarItems = 'SIMILAR_ITEMS',
|
||||
InfiniteAlbumSuggestions = 'INFINITE_ALBUM_SUGGESTIONS',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { JellifyUser } from '@/src/types/JellifyUser'
|
||||
import { SuggestionQueryKeys } from './keys'
|
||||
import { fetchAlbumSuggestions, fetchArtistSuggestions } from './utils/suggestions'
|
||||
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
|
||||
import { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
|
||||
export const DiscoverArtistsQuery = (
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
) => {
|
||||
return {
|
||||
queryKey: [
|
||||
SuggestionQueryKeys.InfiniteArtistSuggestions,
|
||||
user?.id,
|
||||
library?.musicLibraryId,
|
||||
],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchArtistSuggestions(user, library?.musicLibraryId, pageParam),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
maxPages: 2,
|
||||
} as UndefinedInitialDataInfiniteOptions<
|
||||
BaseItemDto[],
|
||||
Error,
|
||||
BaseItemDto[],
|
||||
(string | undefined)[],
|
||||
number
|
||||
>
|
||||
}
|
||||
|
||||
export async function ensureDiscoverArtistsQueryData(
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
) {
|
||||
return await queryClient.ensureInfiniteQueryData(DiscoverArtistsQuery(user, library))
|
||||
}
|
||||
|
||||
export const DiscoverAlbumsQuery = (
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
) => {
|
||||
return {
|
||||
queryKey: [SuggestionQueryKeys.InfiniteAlbumSuggestions, user?.id, library?.musicLibraryId],
|
||||
queryFn: ({ pageParam }) => fetchAlbumSuggestions(user, library?.musicLibraryId, pageParam),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
maxPages: 2,
|
||||
} as UndefinedInitialDataInfiniteOptions<
|
||||
BaseItemDto[],
|
||||
Error,
|
||||
BaseItemDto[],
|
||||
(string | undefined)[],
|
||||
number
|
||||
>
|
||||
}
|
||||
|
||||
export async function ensureDiscoverAlbumsQueryData(
|
||||
user: JellifyUser | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
) {
|
||||
return await queryClient.ensureInfiniteQueryData(DiscoverAlbumsQuery(user, library))
|
||||
}
|
||||
@@ -4,14 +4,16 @@ import { ApiLimits } from '../../../../configs/query.config'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import { getApi } from '../../../../stores'
|
||||
|
||||
export default function fetchSimilarArtists(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
itemId: string,
|
||||
limit: number = ApiLimits.Similar,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (isUndefined(api)) return reject('Client has not been set')
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
|
||||
@@ -31,12 +33,13 @@ export default function fetchSimilarArtists(
|
||||
}
|
||||
|
||||
export function fetchSimilarItems(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
itemId: string,
|
||||
limit: number = ApiLimits.Similar,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (isUndefined(api)) return reject('Client has not been set')
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import { getApi } from '../../../../stores'
|
||||
|
||||
/**
|
||||
* Fetches search suggestions from the Jellyfin server
|
||||
@@ -17,11 +18,12 @@ import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
* @returns A promise of a {@link BaseItemDto} array, be it empty or not
|
||||
*/
|
||||
export async function fetchSearchSuggestions(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
libraryId: string | undefined,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
if (isUndefined(libraryId)) return reject('Library has not been set')
|
||||
@@ -52,12 +54,13 @@ export async function fetchSearchSuggestions(
|
||||
}
|
||||
|
||||
export async function fetchArtistSuggestions(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
libraryId: string | undefined,
|
||||
page: number,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
if (isUndefined(libraryId)) return reject('Library has not been set')
|
||||
@@ -85,12 +88,13 @@ export async function fetchArtistSuggestions(
|
||||
}
|
||||
|
||||
export async function fetchAlbumSuggestions(
|
||||
api: Api | undefined,
|
||||
user: JellifyUser | undefined,
|
||||
libraryId: string | undefined,
|
||||
page: number,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const api = getApi()
|
||||
|
||||
if (isUndefined(api)) return reject('Client instance not set')
|
||||
if (isUndefined(user)) return reject('User has not been set')
|
||||
if (isUndefined(libraryId)) return reject('Library has not been set')
|
||||
|
||||
@@ -17,8 +17,7 @@ const AlbumTemplate = (
|
||||
sectionIndexTitle: disc.title,
|
||||
items: disc.data.map(({ Name, Artists }) => ({
|
||||
text: Name ?? 'Untitled Track',
|
||||
detailText:
|
||||
(Artists?.length ?? 0) > 2 ? formatArtistNames(Artists ?? []) : undefined,
|
||||
detailText: formatArtistNames(Artists ?? []),
|
||||
})),
|
||||
})),
|
||||
onItemSelect: async ({ templateId, index }) => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { CarPlay } from 'react-native-carplay'
|
||||
import { ListTemplate } from 'react-native-carplay/lib/templates/ListTemplate'
|
||||
import AlbumTemplate from './Album'
|
||||
import { ensureAlbumDiscsQuery } from '../../api/queries/album'
|
||||
|
||||
const ArtistTemplate = (artist: BaseItemDto, albums: BaseItemDto[]) =>
|
||||
new ListTemplate({
|
||||
title: artist.Name ?? 'Unknown Artist',
|
||||
sections: [
|
||||
{
|
||||
items: albums.map(({ Name }) => ({
|
||||
text: Name ?? 'Untitled Album',
|
||||
})),
|
||||
},
|
||||
],
|
||||
onItemSelect: async ({ templateId, index }) => {
|
||||
const selectedAlbum = albums[index]
|
||||
|
||||
const albumDiscs = await ensureAlbumDiscsQuery(selectedAlbum)
|
||||
|
||||
CarPlay.pushTemplate(AlbumTemplate(selectedAlbum, albumDiscs), true)
|
||||
},
|
||||
})
|
||||
|
||||
export default ArtistTemplate
|
||||
@@ -1,22 +1,31 @@
|
||||
import { ensureArtistAlbumsQueryData } from '../../api/queries/artist/queries'
|
||||
import { formatArtistName } from '../../utils/formatting/artist-names'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { ListTemplate } from 'react-native-carplay'
|
||||
import { CarPlay, ListTemplate } from 'react-native-carplay'
|
||||
import uuid from 'react-native-uuid'
|
||||
import ArtistTemplate from './Artist'
|
||||
|
||||
const ArtistsTemplate = (items: BaseItemDto[]) =>
|
||||
const ArtistsTemplate = (artists: BaseItemDto[]) =>
|
||||
new ListTemplate({
|
||||
id: uuid.v4(),
|
||||
sections: [
|
||||
{
|
||||
items:
|
||||
items?.map((item) => {
|
||||
artists?.map((artist) => {
|
||||
return {
|
||||
id: item.Id!,
|
||||
text: item.Name ?? 'Untitled',
|
||||
id: artist.Id!,
|
||||
text: formatArtistName(artist.Name),
|
||||
}
|
||||
}) ?? [],
|
||||
},
|
||||
],
|
||||
onItemSelect: async (item) => {},
|
||||
onItemSelect: async ({ index }) => {
|
||||
const artist = artists[index]
|
||||
|
||||
const albums = await ensureArtistAlbumsQueryData(artist)
|
||||
|
||||
CarPlay.pushTemplate(ArtistTemplate(artist, albums), true)
|
||||
},
|
||||
})
|
||||
|
||||
export default ArtistsTemplate
|
||||
|
||||
@@ -1,10 +1,73 @@
|
||||
import { ListTemplate } from 'react-native-carplay'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { getLibrary, getUser } from '../../stores'
|
||||
import { CarPlay, ListTemplate } from 'react-native-carplay'
|
||||
import uuid from 'react-native-uuid'
|
||||
import { SuggestionQueryKeys } from '../../api/queries/suggestions/keys'
|
||||
import {
|
||||
ensureDiscoverAlbumsQueryData,
|
||||
ensureDiscoverArtistsQueryData,
|
||||
} from '../../api/queries/suggestions/queries'
|
||||
import ArtistsTemplate from './Artists'
|
||||
import TracksTemplate from './Tracks'
|
||||
import { ensureRecentlyAddedQueryData } from '../../api/queries/album/queries'
|
||||
|
||||
const CarPlayDiscover = new ListTemplate({
|
||||
id: uuid.v4(),
|
||||
tabTitle: 'Discover',
|
||||
tabSystemImageName: 'globe',
|
||||
sections: [
|
||||
{
|
||||
header: 'Discover',
|
||||
items: [
|
||||
{ id: QueryKeys.RecentlyAdded, text: 'Recently Added' },
|
||||
{ id: SuggestionQueryKeys.InfiniteArtistSuggestions, text: 'Suggested Artists' },
|
||||
{ id: SuggestionQueryKeys.InfiniteAlbumSuggestions, text: 'Suggested Albums' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
onItemSelect: async ({ index }) => {
|
||||
const user = getUser()
|
||||
const library = getLibrary()
|
||||
|
||||
switch (index) {
|
||||
case 0: {
|
||||
// Recently Added
|
||||
const { pages: recentlyAddedPages } = await ensureRecentlyAddedQueryData()
|
||||
|
||||
CarPlay.pushTemplate(
|
||||
TracksTemplate(recentlyAddedPages.flat(), 'Recently Added'),
|
||||
true,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 1: {
|
||||
// Suggested Artists
|
||||
const { pages: suggestedArtistsPages } = await ensureDiscoverArtistsQueryData(
|
||||
user,
|
||||
library,
|
||||
)
|
||||
|
||||
CarPlay.pushTemplate(ArtistsTemplate(suggestedArtistsPages.flat()), true)
|
||||
break
|
||||
}
|
||||
|
||||
case 2: {
|
||||
// Suggested Albums
|
||||
const { pages: suggestedAlbumsPages } = await ensureDiscoverAlbumsQueryData(
|
||||
user,
|
||||
library,
|
||||
)
|
||||
|
||||
CarPlay.pushTemplate(
|
||||
TracksTemplate(suggestedAlbumsPages.flat(), 'More from the Vault'),
|
||||
true,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default CarPlayDiscover
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
FrequentlyPlayedTracksQueryKey,
|
||||
} from '../../api/queries/frequents/keys'
|
||||
import { PlayItAgainQuery } from '../../api/queries/recents'
|
||||
import useJellifyStore, { getUser } from '../../stores'
|
||||
import { getLibrary, getUser } from '../../stores'
|
||||
|
||||
const CarPlayHome = new ListTemplate({
|
||||
id: uuid.v4(),
|
||||
@@ -40,7 +40,7 @@ const CarPlayHome = new ListTemplate({
|
||||
],
|
||||
onItemSelect: async ({ index }) => {
|
||||
const user = getUser()
|
||||
const library = useJellifyStore.getState().library
|
||||
const library = getLibrary()
|
||||
|
||||
switch (index) {
|
||||
case 0: {
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { BaseItemDto, BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { CarPlay, ListTemplate } from 'react-native-carplay'
|
||||
import uuid from 'react-native-uuid'
|
||||
import CarPlayNowPlaying from './NowPlaying'
|
||||
import { Queue } from '../../services/types/queue-item'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { AlbumDiscsQuery } from '../../api/queries/album'
|
||||
import { getApi } from '../../stores'
|
||||
import AlbumTemplate from './Album'
|
||||
import { AlbumDiscsQueryKey } from '../../api/queries/album/keys'
|
||||
import { loadNewQueue } from '../../hooks/player/functions/queue'
|
||||
import { ensureAlbumDiscsQuery } from '../../api/queries/album'
|
||||
import { formatArtistNames } from '../../utils/formatting/artist-names'
|
||||
import { getItemImageUrl } from '../../api/queries/image/utils'
|
||||
|
||||
const TracksTemplate = (items: BaseItemDto[], queuingRef: Queue) =>
|
||||
new ListTemplate({
|
||||
id: uuid.v4(),
|
||||
sections: [
|
||||
{
|
||||
items: items.map(({ Id, Name, Type }) => {
|
||||
const isAlbum = Type === BaseItemKind.MusicAlbum
|
||||
items: items.map((item) => {
|
||||
const isAlbum = item.Type === BaseItemKind.MusicAlbum
|
||||
|
||||
return {
|
||||
id: Id!,
|
||||
text: Name ?? `Untitled ${isAlbum ? 'Album' : 'Track'}`,
|
||||
id: item.Id!,
|
||||
text: item.Name ?? `Untitled ${isAlbum ? 'Album' : 'Track'}`,
|
||||
detailText: formatArtistNames(item.Artists),
|
||||
browsable: isAlbum,
|
||||
accessoryType: isAlbum ? 'disclosure-indicator' : undefined,
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -34,12 +35,9 @@ const TracksTemplate = (items: BaseItemDto[], queuingRef: Queue) =>
|
||||
const startIndex = tracks.indexOf(item)
|
||||
|
||||
if (startIndex === -1) {
|
||||
await queryClient.ensureQueryData(AlbumDiscsQuery(getApi(), item))
|
||||
const albumDiscs = await ensureAlbumDiscsQuery(item)
|
||||
|
||||
CarPlay.pushTemplate(
|
||||
AlbumTemplate(item, queryClient.getQueryData(AlbumDiscsQueryKey(item))!),
|
||||
true,
|
||||
)
|
||||
CarPlay.pushTemplate(AlbumTemplate(item, albumDiscs), true)
|
||||
} else {
|
||||
await loadNewQueue({
|
||||
index: startIndex,
|
||||
|
||||
@@ -3,13 +3,14 @@ import { JellifyUser } from '../types/JellifyUser'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { ONE_DAY, ONE_HOUR, ONE_MINUTE, queryClient } from '../constants/query-client'
|
||||
import { QueryKeys } from '../enums/query-keys'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../api/queries/item'
|
||||
import { fetchItem } from '../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import fetchUserData from '../api/queries/user-data/utils'
|
||||
import UserDataQueryKey from '../api/queries/user-data/keys'
|
||||
import { getApi, getUser } from '../stores'
|
||||
import { ArtistQueryKey } from '../api/queries/artist/keys'
|
||||
import { AlbumQuery } from '../api/queries/album/queries'
|
||||
import { ensureAlbumDiscsQuery } from '../api/queries/album'
|
||||
|
||||
// Module-level dedup guard — no hook needed, this is just a long-lived Set
|
||||
const prefetchedContext = new Set<string>()
|
||||
@@ -81,11 +82,7 @@ function warmAlbumContext(api: Api | undefined, album: BaseItemDto): void {
|
||||
const albumDiscsQueryKey = [QueryKeys.ItemTracks, Id]
|
||||
|
||||
if (queryClient.getQueryState(albumDiscsQueryKey)?.status !== 'success')
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: albumDiscsQueryKey,
|
||||
queryFn: () => fetchAlbumDiscs(api, album),
|
||||
staleTime: ONE_DAY,
|
||||
})
|
||||
ensureAlbumDiscsQuery(album)
|
||||
}
|
||||
|
||||
function warmArtistContext(api: Api | undefined, artistId: string): void {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { createContext, ReactNode, use } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useArtistAlbums, useArtistFeaturedOn } from '../../api/queries/artist'
|
||||
import { useJellifyLibrary, getApi, getUser } from '../../stores'
|
||||
import { getUser } from '../../stores'
|
||||
|
||||
interface ArtistContext {
|
||||
fetchingAlbums: boolean
|
||||
@@ -36,9 +36,7 @@ export const ArtistProvider = ({
|
||||
artist: BaseItemDto
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const {
|
||||
data: albums,
|
||||
@@ -58,7 +56,7 @@ export const ArtistProvider = ({
|
||||
isPending: fetchingSimilarArtists,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.SimilarItems, artist.Id],
|
||||
queryFn: () => fetchSimilarArtists(api, user, artist.Id!),
|
||||
queryFn: () => fetchSimilarArtists(user, artist.Id!),
|
||||
enabled: !isUndefined(artist.Id),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user