Add track results to search queries
This commit is contained in:
@@ -38,5 +38,6 @@
|
|||||||
"enable-error-reporting-description": "This helps improve the app experience by sending crash and error reports to us.",
|
"enable-error-reporting-description": "This helps improve the app experience by sending crash and error reports to us.",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"more-info": "More Info"
|
"more-info": "More Info",
|
||||||
|
"track": "Track"
|
||||||
}
|
}
|
||||||
@@ -37,4 +37,5 @@ export type LocaleKeys = 'play-next'
|
|||||||
| 'enable-error-reporting-description'
|
| 'enable-error-reporting-description'
|
||||||
| 'enable'
|
| 'enable'
|
||||||
| 'disable'
|
| 'disable'
|
||||||
| 'more-info'
|
| 'more-info'
|
||||||
|
| 'track'
|
||||||
@@ -2,9 +2,9 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import Input from 'components/Input';
|
import Input from 'components/Input';
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
import styled from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { useTypedSelector } from 'store';
|
import { useAppDispatch, useTypedSelector } from 'store';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { Album } from 'store/music/types';
|
import { Album, AlbumTrack } from 'store/music/types';
|
||||||
import { FlatList } from 'react-native-gesture-handler';
|
import { FlatList } from 'react-native-gesture-handler';
|
||||||
import TouchableHandler from 'components/TouchableHandler';
|
import TouchableHandler from 'components/TouchableHandler';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
@@ -13,6 +13,7 @@ import { NavigationProp } from '../types';
|
|||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import useDefaultStyles from 'components/Colors';
|
import useDefaultStyles from 'components/Colors';
|
||||||
|
import { searchAndFetchAlbums } from 'store/music/actions';
|
||||||
|
|
||||||
const Container = styled.View`
|
const Container = styled.View`
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
@@ -46,18 +47,37 @@ const fuseOptions = {
|
|||||||
includeScore: true,
|
includeScore: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AudioResult = {
|
||||||
|
type: 'Audio',
|
||||||
|
id: string;
|
||||||
|
album: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AlbumResult = {
|
||||||
|
type: 'AlbumArtist',
|
||||||
|
id: string;
|
||||||
|
album: undefined;
|
||||||
|
name: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CombinedResults = (AudioResult | AlbumResult)[];
|
||||||
|
|
||||||
export default function Search() {
|
export default function Search() {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
|
|
||||||
// Prepare state
|
// Prepare state
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||||
const [results, setResults] = useState<Fuse.FuseResult<Album>[]>([]);
|
const [results, setResults] = useState<CombinedResults>([]);
|
||||||
const fuse = useRef<Fuse<Album, typeof fuseOptions>>();
|
// const [isLoading, setLoading] = useState(false);
|
||||||
|
const fuse = useRef<Fuse<Album>>();
|
||||||
|
|
||||||
// Prepare helpers
|
// Prepare helpers
|
||||||
const navigation = useNavigation<NavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
|
const credentials = useTypedSelector(state => state.settings.jellyfin);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since it is impractical to have a global fuse variable, we need to
|
* Since it is impractical to have a global fuse variable, we need to
|
||||||
@@ -75,14 +95,52 @@ export default function Search() {
|
|||||||
* them to state
|
* them to state
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// GUARD: In some extraordinary cases, Fuse might not be presented since
|
const retrieveResults = async () => {
|
||||||
// it is assigned via refs. In this case, we can't handle any searching.
|
// GUARD: In some extraordinary cases, Fuse might not be presented since
|
||||||
if (!fuse.current) {
|
// it is assigned via refs. In this case, we can't handle any searching.
|
||||||
return;
|
if (!fuse.current) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setResults(fuse.current.search(searchTerm));
|
// First set the immediate results from fuse
|
||||||
}, [searchTerm, setResults, fuse]);
|
const fuseResults = fuse.current.search(searchTerm);
|
||||||
|
const albums: AlbumResult[] = fuseResults
|
||||||
|
.map(({ item }) => ({
|
||||||
|
id: item.Id,
|
||||||
|
type: 'AlbumArtist',
|
||||||
|
album: undefined,
|
||||||
|
name: undefined,
|
||||||
|
}));
|
||||||
|
const albumIds = fuseResults.map(({ item }) => item.Id);
|
||||||
|
|
||||||
|
// Assign the preliminary results
|
||||||
|
setResults(albums);
|
||||||
|
|
||||||
|
// Then query the Jellyfin API
|
||||||
|
const { payload } = await dispatch(searchAndFetchAlbums({ term: searchTerm }));
|
||||||
|
|
||||||
|
const items = (payload as
|
||||||
|
{ results: (Album | AlbumTrack)[] }
|
||||||
|
).results.filter(item => (
|
||||||
|
!(item.Type === 'MusicAlbum' && albumIds.includes(item.Id))
|
||||||
|
)).map((item) => ({
|
||||||
|
type: item.Type,
|
||||||
|
id: item.Id,
|
||||||
|
album: item.Type === 'Audio'
|
||||||
|
? item.AlbumId
|
||||||
|
: undefined,
|
||||||
|
name: item.Type === 'Audio'
|
||||||
|
? item.Name
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Then add those to the results
|
||||||
|
// console.log(results, items);
|
||||||
|
setResults([...albums, ...items] as CombinedResults);
|
||||||
|
};
|
||||||
|
|
||||||
|
retrieveResults();
|
||||||
|
}, [searchTerm, setResults, fuse, credentials, dispatch]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const selectAlbum = useCallback((id: string) =>
|
const selectAlbum = useCallback((id: string) =>
|
||||||
@@ -92,7 +150,9 @@ export default function Search() {
|
|||||||
const HeaderComponent = React.useMemo(() => (
|
const HeaderComponent = React.useMemo(() => (
|
||||||
<Container>
|
<Container>
|
||||||
<Input value={searchTerm} onChangeText={setSearchTerm} style={defaultStyles.input} placeholder={t('search') + '...'} />
|
<Input value={searchTerm} onChangeText={setSearchTerm} style={defaultStyles.input} placeholder={t('search') + '...'} />
|
||||||
{(searchTerm.length && !results.length) ? <Text style={{ textAlign: 'center' }}>{t('no-results')}</Text> : null}
|
{(searchTerm.length && !results.length)
|
||||||
|
? <Text style={{ textAlign: 'center' }}>{t('no-results')}</Text>
|
||||||
|
: null}
|
||||||
</Container>
|
</Container>
|
||||||
), [searchTerm, results, setSearchTerm, defaultStyles]);
|
), [searchTerm, results, setSearchTerm, defaultStyles]);
|
||||||
|
|
||||||
@@ -103,24 +163,37 @@ export default function Search() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<>
|
||||||
data={results}
|
<FlatList
|
||||||
renderItem={({ item: { item: album } }) =>(
|
data={results}
|
||||||
<TouchableHandler id={album.Id} onPress={selectAlbum}>
|
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
||||||
<SearchResult style={defaultStyles.border}>
|
const album = albums[trackAlbum || id];
|
||||||
<AlbumImage source={{ uri: getImage(album.Id) }} />
|
|
||||||
<View>
|
if (!album) {
|
||||||
<Text numberOfLines={1} ellipsizeMode="tail" style={defaultStyles.text}>
|
console.log('Couldnt find ', trackAlbum, id);
|
||||||
{album.Name} - {album.AlbumArtist}
|
return null;
|
||||||
</Text>
|
}
|
||||||
<HalfOpacity style={defaultStyles.text}>{t('album')}</HalfOpacity>
|
|
||||||
</View>
|
return (
|
||||||
</SearchResult>
|
<TouchableHandler id={album.Id} onPress={selectAlbum}>
|
||||||
</TouchableHandler>
|
<SearchResult style={defaultStyles.border}>
|
||||||
)}
|
<AlbumImage source={{ uri: getImage(album.Id) }} />
|
||||||
keyExtractor={(item) => item.refIndex.toString()}
|
<View>
|
||||||
ListHeaderComponent={HeaderComponent}
|
<Text numberOfLines={1} ellipsizeMode="tail" style={defaultStyles.text}>
|
||||||
extraData={searchTerm}
|
{trackName || album.Name} - {album.AlbumArtist}
|
||||||
/>
|
</Text>
|
||||||
|
<HalfOpacity style={defaultStyles.text}>
|
||||||
|
{type === 'AlbumArtist' ? t('album'): t('track')}
|
||||||
|
</HalfOpacity>
|
||||||
|
</View>
|
||||||
|
</SearchResult>
|
||||||
|
</TouchableHandler>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
ListHeaderComponent={HeaderComponent}
|
||||||
|
extraData={[searchTerm, albums]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { configureStore, getDefaultMiddleware, combineReducers } from '@reduxjs/toolkit';
|
import { configureStore, getDefaultMiddleware, combineReducers } from '@reduxjs/toolkit';
|
||||||
import { useSelector, TypedUseSelectorHook } from 'react-redux';
|
import { useSelector, TypedUseSelectorHook, useDispatch } from 'react-redux';
|
||||||
import AsyncStorage from '@react-native-community/async-storage';
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
import { persistStore, persistReducer, PersistConfig } from 'redux-persist';
|
import { persistStore, persistReducer, PersistConfig } from 'redux-persist';
|
||||||
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
|
import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2';
|
||||||
@@ -34,6 +34,7 @@ export type AppState = ReturnType<typeof reducers>;
|
|||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
|
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
|
||||||
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
|
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
|
||||||
export const persistedStore = persistStore(store);
|
export const persistedStore = persistStore(store);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
|
||||||
import { Album, AlbumTrack } from './types';
|
import { Album, AlbumTrack } from './types';
|
||||||
import { AsyncThunkAPI } from '..';
|
import { AsyncThunkAPI } from '..';
|
||||||
import { retrieveAlbums, retrieveAlbumTracks, retrieveRecentAlbums } from 'utility/JellyfinApi';
|
import { retrieveAllAlbums, retrieveAlbumTracks, retrieveRecentAlbums, searchItem, retrieveAlbum } from 'utility/JellyfinApi';
|
||||||
|
|
||||||
export const albumAdapter = createEntityAdapter<Album>({
|
export const albumAdapter = createEntityAdapter<Album>({
|
||||||
selectId: album => album.Id,
|
selectId: album => album.Id,
|
||||||
@@ -15,7 +15,7 @@ export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI
|
|||||||
'/albums/all',
|
'/albums/all',
|
||||||
async (empty, thunkAPI) => {
|
async (empty, thunkAPI) => {
|
||||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||||
return retrieveAlbums(credentials) as Promise<Album[]>;
|
return retrieveAllAlbums(credentials) as Promise<Album[]>;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,4 +44,36 @@ export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncTh
|
|||||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||||
return retrieveAlbumTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
|
return retrieveAlbumTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type SearchAndFetchResults = {
|
||||||
|
albums: Album[];
|
||||||
|
results: (Album | AlbumTrack)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchAndFetchAlbums = createAsyncThunk<
|
||||||
|
SearchAndFetchResults,
|
||||||
|
{ term: string, limit?: number },
|
||||||
|
AsyncThunkAPI
|
||||||
|
>(
|
||||||
|
'/search',
|
||||||
|
async ({ term, limit = 24 }, thunkAPI) => {
|
||||||
|
const state = thunkAPI.getState();
|
||||||
|
const results = await searchItem(state.settings.jellyfin, term, limit);
|
||||||
|
|
||||||
|
const albums = await Promise.all(results.filter((item) => (
|
||||||
|
!state.music.albums.ids.includes(item.Type === 'MusicAlbum' ? item.Id : item.AlbumId)
|
||||||
|
)).map(async (item) => {
|
||||||
|
if (item.Type === 'MusicAlbum') {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return retrieveAlbum(state.settings.jellyfin, item.AlbumId);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
albums,
|
||||||
|
results
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fetchAllAlbums, albumAdapter, fetchTracksByAlbum, trackAdapter, fetchRecentAlbums } from './actions';
|
import { fetchAllAlbums, albumAdapter, fetchTracksByAlbum, trackAdapter, fetchRecentAlbums, searchAndFetchAlbums } from './actions';
|
||||||
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
|
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
|
||||||
import { Album, AlbumTrack } from './types';
|
import { Album, AlbumTrack } from './types';
|
||||||
import { setJellyfinCredentials } from 'store/settings/actions';
|
import { setJellyfinCredentials } from 'store/settings/actions';
|
||||||
@@ -77,6 +77,11 @@ const music = createSlice({
|
|||||||
builder.addCase(fetchTracksByAlbum.pending, (state) => { state.tracks.isLoading = true; });
|
builder.addCase(fetchTracksByAlbum.pending, (state) => { state.tracks.isLoading = true; });
|
||||||
builder.addCase(fetchTracksByAlbum.rejected, (state) => { state.tracks.isLoading = false; });
|
builder.addCase(fetchTracksByAlbum.rejected, (state) => { state.tracks.isLoading = false; });
|
||||||
|
|
||||||
|
builder.addCase(searchAndFetchAlbums.fulfilled, (state, { payload }) => {
|
||||||
|
console.log('INSERTING', payload.albums);
|
||||||
|
albumAdapter.upsertMany(state.albums, payload.albums);
|
||||||
|
});
|
||||||
|
|
||||||
// Reset any caches we have when a new server is set
|
// Reset any caches we have when a new server is set
|
||||||
builder.addCase(setJellyfinCredentials, () => initialState);
|
builder.addCase(setJellyfinCredentials, () => initialState);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export interface Album {
|
|||||||
RunTimeTicks: number;
|
RunTimeTicks: number;
|
||||||
ProductionYear: number;
|
ProductionYear: number;
|
||||||
IsFolder: boolean;
|
IsFolder: boolean;
|
||||||
Type: string;
|
Type: 'MusicAlbum';
|
||||||
UserData: UserData;
|
UserData: UserData;
|
||||||
PrimaryImageAspectRatio: number;
|
PrimaryImageAspectRatio: number;
|
||||||
Artists: string[];
|
Artists: string[];
|
||||||
@@ -53,7 +53,7 @@ export interface AlbumTrack {
|
|||||||
ProductionYear: number;
|
ProductionYear: number;
|
||||||
IndexNumber: number;
|
IndexNumber: number;
|
||||||
IsFolder: boolean;
|
IsFolder: boolean;
|
||||||
Type: string;
|
Type: 'Audio';
|
||||||
UserData: UserData;
|
UserData: UserData;
|
||||||
Artists: string[];
|
Artists: string[];
|
||||||
ArtistItems: ArtistItem[];
|
ArtistItems: ArtistItem[];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Track } from 'react-native-track-player';
|
import { Track } from 'react-native-track-player';
|
||||||
import { AppState, useTypedSelector } from 'store';
|
import { AppState, useTypedSelector } from 'store';
|
||||||
import { AlbumTrack } from 'store/music/types';
|
import { Album, AlbumTrack } from 'store/music/types';
|
||||||
|
|
||||||
type Credentials = AppState['settings']['jellyfin'];
|
type Credentials = AppState['settings']['jellyfin'];
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ const albumParams = new URLSearchParams(albumOptions).toString();
|
|||||||
/**
|
/**
|
||||||
* Retrieve all albums that are available on the Jellyfin server
|
* Retrieve all albums that are available on the Jellyfin server
|
||||||
*/
|
*/
|
||||||
export async function retrieveAlbums(credentials: Credentials) {
|
export async function retrieveAllAlbums(credentials: Credentials) {
|
||||||
const config = generateConfig(credentials);
|
const config = generateConfig(credentials);
|
||||||
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${albumParams}`, config)
|
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${albumParams}`, config)
|
||||||
.then(response => response.json());
|
.then(response => response.json());
|
||||||
@@ -78,6 +78,15 @@ export async function retrieveAlbums(credentials: Credentials) {
|
|||||||
return albums.Items;
|
return albums.Items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a single album
|
||||||
|
*/
|
||||||
|
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
|
||||||
|
const config = generateConfig(credentials);
|
||||||
|
return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
const latestAlbumsOptions = {
|
const latestAlbumsOptions = {
|
||||||
IncludeItemTypes: 'MusicAlbum',
|
IncludeItemTypes: 'MusicAlbum',
|
||||||
Fields: 'DateCreated',
|
Fields: 'DateCreated',
|
||||||
@@ -122,11 +131,66 @@ export async function retrieveAlbumTracks(ItemId: string, credentials: Credentia
|
|||||||
return album.Items;
|
return album.Items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an image URL for a given ItemId
|
||||||
|
*/
|
||||||
export function getImage(ItemId: string, credentials: Credentials): string {
|
export function getImage(ItemId: string, credentials: Credentials): string {
|
||||||
return encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
return encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a hook that can convert ItemIds to image URLs
|
||||||
|
*/
|
||||||
export function useGetImage() {
|
export function useGetImage() {
|
||||||
const credentials = useTypedSelector((state) => state.settings.jellyfin);
|
const credentials = useTypedSelector((state) => state.settings.jellyfin);
|
||||||
return (ItemId: string) => getImage(ItemId, credentials);
|
return (ItemId: string) => getImage(ItemId, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trackParams = {
|
||||||
|
SortBy: 'AlbumArtist,SortName',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
Recursive: 'true',
|
||||||
|
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all possible tracks that can be found in Jellyfin
|
||||||
|
*/
|
||||||
|
export async function retrieveAllTracks(credentials: Credentials) {
|
||||||
|
const config = generateConfig(credentials);
|
||||||
|
const tracks = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${trackParams}`, config)
|
||||||
|
.then(response => response.json());
|
||||||
|
|
||||||
|
return tracks.Items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = {
|
||||||
|
IncludeItemTypes: 'Audio,MusicAlbum',
|
||||||
|
SortBy: 'Album,SortName',
|
||||||
|
SortOrder: 'Ascending',
|
||||||
|
Recursive: 'true',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remotely search the Jellyfin library for a particular search term
|
||||||
|
*/
|
||||||
|
export async function searchItem(
|
||||||
|
credentials: Credentials,
|
||||||
|
term: string, limit = 24
|
||||||
|
): Promise<(Album | AlbumTrack)[]> {
|
||||||
|
const config = generateConfig(credentials);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
SearchTerm: term,
|
||||||
|
Limit: limit.toString(),
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
const results = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${params}`, config)
|
||||||
|
.then(response => response.json());
|
||||||
|
|
||||||
|
return results.Items;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user