Add label list in the music filters

pull/3800/merge^2
Anatole Sot 3 months ago
parent 512ac9bfb4
commit 2b78dd9ace

@ -4435,6 +4435,11 @@ paths:
type: number
example: 1
default: 1
- in: query
name: type
schema:
type: string
enum: [movie,tv,music]
responses:
'200':
description: Results
@ -4455,7 +4460,9 @@ paths:
results:
type: array
items:
$ref: '#/components/schemas/Keyword'
oneOf:
- $ref: '#/components/schemas/Keyword'
- type: string
/search/company:
get:
summary: Search for companies

@ -1,9 +1,11 @@
import logger from '@server/logger';
import type {
ArtistSearchResponse,
luceneSearchOptions,
RecordingSearchResponse,
ReleaseGroupSearchResponse,
ReleaseSearchResponse,
TagSearchResponse,
WorkSearchResponse,
} from 'nodebrainz';
import BaseNodeBrainz from 'nodebrainz';
@ -29,14 +31,14 @@ import { mbArtistType, mbReleaseGroupType, mbWorkType } from './interfaces';
interface ArtistSearchOptions {
query: string;
tag?: string; // (part of) a tag attached to the artist
tags?: string[]; // (part of) a tag attached to the artist
limit?: number;
offset?: number;
}
interface RecordingSearchOptions {
query: string;
tag?: string; // (part of) a tag attached to the recording
tags?: string[]; // (part of) a tag attached to the recording
artistname?: string; // (part of) the name of any of the recording artists
release?: string; // the name of a release that the recording appears on
offset?: number;
@ -46,7 +48,7 @@ interface RecordingSearchOptions {
interface ReleaseSearchOptions {
query: string;
artistname?: string; // (part of) the name of any of the release artists
tag?: string; // (part of) a tag attached to the release
tags?: string[]; // (part of) a tag attached to the release
limit?: number;
offset?: number;
}
@ -54,7 +56,7 @@ interface ReleaseSearchOptions {
interface ReleaseGroupSearchOptions {
query: string;
artistname?: string; // (part of) the name of any of the release group artists
tag?: string; // (part of) a tag attached to the release group
tags?: string[]; // (part of) a tag attached to the release group
limit?: number;
offset?: number;
}
@ -62,7 +64,7 @@ interface ReleaseGroupSearchOptions {
interface WorkSearchOptions {
query: string;
artist?: string; // (part of) the name of an artist related to the work (e.g. a composer or lyricist)
tag?: string; // (part of) a tag attached to the work
tags?: string[]; // (part of) a tag attached to the work
limit?: number;
offset?: number;
}
@ -73,34 +75,8 @@ function searchOptionstoArtistSearchOptions(
const data: ArtistSearchOptions = {
query: options.query,
};
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
} else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page - 1) * data.limit;
}
return data;
}
function searchOptionstoRecordingSearchOptions(
options: SearchOptions
): RecordingSearchOptions {
const data: RecordingSearchOptions = {
query: options.query,
};
if (options.tag) {
data.tag = options.tag;
}
if (options.artistname) {
data.artistname = options.artistname;
}
if (options.albumname) {
data.release = options.albumname;
if (options.tags) {
data.tags = options.tags;
}
if (options.limit) {
data.limit = options.limit;
@ -122,54 +98,8 @@ function searchOptionstoReleaseSearchOptions(
if (options.artistname) {
data.artistname = options.artistname;
}
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
} else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page - 1) * data.limit;
}
return data;
}
function searchOptionstoReleaseGroupSearchOptions(
options: SearchOptions
): ReleaseGroupSearchOptions {
const data: ReleaseGroupSearchOptions = {
query: options.query,
};
if (options.artistname) {
data.artistname = options.artistname;
}
if (options.tag) {
data.tag = options.tag;
}
if (options.limit) {
data.limit = options.limit;
} else {
data.limit = 25;
}
if (options.page) {
data.offset = (options.page - 1) * data.limit;
}
return data;
}
function searchOptionstoWorkSearchOptions(
options: SearchOptions
): WorkSearchOptions {
const data: WorkSearchOptions = {
query: options.query,
};
if (options.artistname) {
data.artist = options.artistname;
}
if (options.tag) {
data.tag = options.tag;
if (options.tags) {
data.tags = options.tags;
}
if (options.limit) {
data.limit = options.limit;
@ -292,6 +222,37 @@ function convertTrack(track: Track): mbRecording {
};
}
function processReleaseSearchParams(
search: ReleaseSearchOptions
): luceneSearchOptions {
const processedSearchParams: luceneSearchOptions = {
query: search.query,
limit: search.limit,
offset: search.offset,
};
if (search.artistname) {
processedSearchParams.query += ` AND artist:${search.artistname}`;
}
if (search.tags) {
processedSearchParams.query += ` AND tag:${search.tags.join(' AND tag:')}`;
}
return processedSearchParams;
}
function processArtistSearchParams(
search: ArtistSearchOptions
): luceneSearchOptions {
const processedSearchParams: luceneSearchOptions = {
query: search.query,
limit: search.limit,
offset: search.offset,
};
if (search.tags) {
processedSearchParams.query += ` AND tag:${search.tags.join(' AND tag:')}`;
}
return processedSearchParams;
}
class MusicBrainz extends BaseNodeBrainz {
constructor() {
super({
@ -306,26 +267,14 @@ class MusicBrainz extends BaseNodeBrainz {
public searchMulti = async (search: SearchOptions) => {
try {
const artistSearch = searchOptionstoArtistSearchOptions(search);
const recordingSearch = searchOptionstoRecordingSearchOptions(search);
const releaseGroupSearch =
searchOptionstoReleaseGroupSearchOptions(search);
const releaseSearch = searchOptionstoReleaseSearchOptions(search);
const workSearch = searchOptionstoWorkSearchOptions(search);
const artistResults = await this.searchArtists(artistSearch);
const recordingResults = await this.searchRecordings(recordingSearch);
const releaseGroupResults = await this.searchReleaseGroups(
releaseGroupSearch
);
const releaseResults = await this.searchReleases(releaseSearch);
const workResults = await this.searchWorks(workSearch);
const combinedResults = {
status: 'ok',
artistResults,
recordingResults,
releaseGroupResults,
releaseResults,
workResults,
};
return combinedResults;
@ -333,10 +282,7 @@ class MusicBrainz extends BaseNodeBrainz {
return {
status: 'error',
artistResults: [],
recordingResults: [],
releaseGroupResults: [],
releaseResults: [],
workResults: [],
};
}
};
@ -346,7 +292,8 @@ class MusicBrainz extends BaseNodeBrainz {
): Promise<mbArtist[]> => {
try {
return await new Promise<mbArtist[]>((resolve, reject) => {
this.search('artist', search, (error, data) => {
const processedSearch = processArtistSearchParams(search);
this.luceneSearch('artist', processedSearch, (error, data) => {
if (error) {
reject(error);
} else {
@ -418,8 +365,9 @@ class MusicBrainz extends BaseNodeBrainz {
search: ReleaseSearchOptions
): Promise<mbRelease[]> => {
try {
const processedSearchParams = processReleaseSearchParams(search);
return new Promise<mbRelease[]>((resolve, reject) => {
this.search('release', search, (error, data) => {
this.luceneSearch('release', processedSearchParams, (error, data) => {
if (error) {
reject(error);
} else {
@ -460,6 +408,28 @@ class MusicBrainz extends BaseNodeBrainz {
}
};
public searchTags = (query: string): Promise<string[]> => {
try {
return new Promise<string[]>((resolve, reject) => {
this.search('tag', { tag: query }, (error, data) => {
if (error) {
reject(error);
} else {
const rawResults = data as TagSearchResponse;
const results = rawResults.tags.map((tag) => tag.name);
resolve(results);
}
});
});
} catch (e) {
logger.error('Failed to search for tags', {
label: 'MusicBrainz',
message: e.message,
});
return new Promise<string[]>((resolve) => resolve([]));
}
};
public getArtist = (artistId: string): Promise<mbArtist> => {
try {
return new Promise<mbArtist>((resolve, reject) => {

@ -282,5 +282,5 @@ export interface SearchOptions {
artistname?: string;
albumname?: string;
recordingname?: string;
tag?: string;
tags?: string[];
}

@ -15,6 +15,7 @@ import { getSettings } from '@server/lib/settings';
import logger from '@server/logger';
import { mapProductionCompany } from '@server/models/Movie';
import {
mapArtistResult,
mapCollectionResult,
mapMovieResult,
mapPersonResult,
@ -857,27 +858,47 @@ discoverRoutes.get('/musics', async (req, res, next) => {
const mb = new MusicBrainz();
try {
const query = QueryFilterOptions.parse(req.query);
const keywords = query.keywords;
const results = await mb.searchReleases({
query: keywords ?? '',
const results = await mb.searchMulti({
query: '',
tags: query.keywords ? decodeURIComponent(query.keywords).split(',') : [],
limit: 20,
offset: (Number(query.page) - 1) * 20,
page: Number(query.page),
});
const mbIds = results.map((result) => result.id);
const mbIds = results.releaseResults
.map((result) => result.id)
.concat(results.artistResults.map((result) => result.id));
const media = await Media.getRelatedMedia([], mbIds);
return res.status(200).json({
page: query.page,
results: await Promise.all(
results.map((result) => {
const resultsWithMedia = [
...(await Promise.all(
results.artistResults.map((result) => {
return mapArtistResult(
result,
media.find(
(med) =>
med.mbId === result.id &&
med.mediaType === MediaType.MUSIC &&
med.secondaryType === 'artist'
)
);
})
)),
...(await Promise.all(
results.releaseResults.map((result) => {
return mapReleaseResult(
result,
media.find(
(med) =>
med.mbId === result.id && med.mediaType === MediaType.MUSIC
med.mbId === result.id &&
med.mediaType === MediaType.MUSIC &&
med.secondaryType === 'release'
)
);
})
),
)),
];
return res.status(200).json({
page: query.page,
results: resultsWithMedia,
});
} catch (e) {
logger.debug('Something went wrong retrieving release groups', {

@ -1,3 +1,4 @@
import MusicBrainz from '@server/api/musicbrainz';
import TheMovieDb from '@server/api/themoviedb';
import type { TmdbSearchMultiResponse } from '@server/api/themoviedb/interfaces';
import Media from '@server/entity/Media';
@ -57,15 +58,22 @@ searchRoutes.get('/', async (req, res, next) => {
});
searchRoutes.get('/keyword', async (req, res, next) => {
const tmdb = new TheMovieDb();
try {
const results = await tmdb.searchKeyword({
query: req.query.query as string,
page: Number(req.query.page),
});
if (!req.query.type || req.query.type !== 'music') {
const tmdb = new TheMovieDb();
const results = await tmdb.searchKeyword({
query: req.query.query as string,
page: Number(req.query.page),
});
return res.status(200).json(results);
return res.status(200).json(results);
} else {
const mb = new MusicBrainz();
const results = await mb.searchTags(req.query.query as string);
return res.status(200).json(results);
}
} catch (e) {
logger.debug('Something went wrong retrieving keyword search results', {
label: 'API',

@ -28,6 +28,13 @@ declare module 'nodebrainz' {
works: Work[];
}
export interface TagSearchResponse extends RawSearchResponse {
tags: {
score: number;
name: string;
}[];
}
export interface BrowseRequestParams {
limit?: number;
offset?: number;
@ -40,6 +47,12 @@ declare module 'nodebrainz' {
[key: string]: string | number | undefined;
}
export interface luceneSearchOptions {
query: string;
limit?: number;
offset?: number;
}
export default class BaseNodeBrainz {
constructor(options: {
userAgent: string;
@ -83,6 +96,7 @@ declare module 'nodebrainz' {
| RecordingSearchResponse
| ReleaseGroupSearchResponse
| WorkSearchResponse
| TagSearchResponse
) => void
): Promise<Artist[] | Release[] | Recording[] | Group[] | Work[]>;
browse(
@ -118,5 +132,19 @@ declare module 'nodebrainz' {
}
) => void
): Promise<Artist[] | Release[] | Recording[] | Group[] | Work[]>;
luceneSearch(
type: string,
search: luceneSearchOptions,
callback: (
err: Error,
data:
| ArtistSearchResponse
| ReleaseSearchResponse
| RecordingSearchResponse
| ReleaseGroupSearchResponse
| WorkSearchResponse
| TagSearchResponse
) => void
): Promise<Artist[] | Release[] | Recording[] | Group[] | Work[]>;
}
}

@ -74,7 +74,9 @@ const DiscoverMusics = () => {
</div>
</div>
</div>
<RecentlyAddedSlider type="artist" />
{Object.keys(preparedFilters).length === 0 && (
<RecentlyAddedSlider type="artist" />
)}
<div className="slider-header">
<div className="slider-title">
<span>{intl.formatMessage(messages.discovermoremusics)}</span>

@ -74,59 +74,59 @@ const FilterSlideover = ({
onClose={() => onClose()}
>
<div className="flex flex-col space-y-4">
{ type !== 'music' &&
(<div>
{type !== 'music' && (
<div>
<div className="mb-2 text-lg font-semibold">
{intl.formatMessage(
type === 'movie' ? messages.releaseDate : messages.firstAirDate
)}
</div>
<div className="relative z-40 flex space-x-2">
<div className="flex flex-col">
<div className="mb-2">{intl.formatMessage(messages.from)}</div>
<Datepicker
primaryColor="indigo"
value={{
startDate: currentFilters[dateGte] ?? null,
endDate: currentFilters[dateGte] ?? null,
}}
onChange={(value) => {
updateQueryParams(
dateGte,
value?.startDate ? (value.startDate as string) : undefined
);
}}
inputName="fromdate"
useRange={false}
asSingle
containerClassName="datepicker-wrapper"
inputClassName="pr-1 sm:pr-4 text-base leading-5"
/>
</div>
<div className="flex flex-col">
<div className="mb-2">{intl.formatMessage(messages.to)}</div>
<Datepicker
primaryColor="indigo"
value={{
startDate: currentFilters[dateLte] ?? null,
endDate: currentFilters[dateLte] ?? null,
}}
onChange={(value) => {
updateQueryParams(
dateLte,
value?.startDate ? (value.startDate as string) : undefined
);
}}
inputName="todate"
useRange={false}
asSingle
containerClassName="datepicker-wrapper"
inputClassName="pr-1 sm:pr-4 text-base leading-5"
/>
<div className="relative z-40 flex space-x-2">
<div className="flex flex-col">
<div className="mb-2">{intl.formatMessage(messages.from)}</div>
<Datepicker
primaryColor="indigo"
value={{
startDate: currentFilters[dateGte] ?? null,
endDate: currentFilters[dateGte] ?? null,
}}
onChange={(value) => {
updateQueryParams(
dateGte,
value?.startDate ? (value.startDate as string) : undefined
);
}}
inputName="fromdate"
useRange={false}
asSingle
containerClassName="datepicker-wrapper"
inputClassName="pr-1 sm:pr-4 text-base leading-5"
/>
</div>
<div className="flex flex-col">
<div className="mb-2">{intl.formatMessage(messages.to)}</div>
<Datepicker
primaryColor="indigo"
value={{
startDate: currentFilters[dateLte] ?? null,
endDate: currentFilters[dateLte] ?? null,
}}
onChange={(value) => {
updateQueryParams(
dateLte,
value?.startDate ? (value.startDate as string) : undefined
);
}}
inputName="todate"
useRange={false}
asSingle
containerClassName="datepicker-wrapper"
inputClassName="pr-1 sm:pr-4 text-base leading-5"
/>
</div>
</div>
</div>
</div>)
}
)}
{type === 'movie' && (
<>
<span className="text-lg font-semibold">
@ -140,188 +140,199 @@ const FilterSlideover = ({
/>
</>
)}
{ type !== 'music' && (
<>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.genres)}
</span>
<GenreSelector
type={type}
defaultValue={currentFilters.genre}
isMulti
onChange={(value) => {
updateQueryParams('genre', value?.map((v) => v.value).join(','));
}}
/>
</>)}
{type !== 'music' && (
<>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.genres)}
</span>
<GenreSelector
type={type}
defaultValue={currentFilters.genre}
isMulti
onChange={(value) => {
updateQueryParams(
'genre',
value?.map((v) => v.value).join(',')
);
}}
/>
</>
)}
<span className="text-lg font-semibold">
{intl.formatMessage(messages.keywords)}
</span>
<KeywordSelector
defaultValue={currentFilters.keywords}
isMulti
type={type}
onChange={(value) => {
updateQueryParams('keywords', type === 'music' ? value?.map((v) => v.label).join(' ') : value?.map((v) => v.value).join(','));
updateQueryParams(
'keywords',
type === 'music'
? encodeURIComponent(value?.map((v) => v.label).join(',') ?? '')
: encodeURIComponent(value?.map((v) => v.value).join(',') ?? '')
);
}}
/>
{ type !== 'music' && (
<>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.originalLanguage)}
</span>
<LanguageSelector
value={currentFilters.language}
serverValue={currentSettings.originalLanguage}
isUserSettings
setFieldValue={(_key, value) => {
updateQueryParams('language', value);
}}
/>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.runtime)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={0}
max={400}
onUpdateMin={(min) => {
updateQueryParams(
'withRuntimeGte',
min !== 0 && Number(currentFilters.withRuntimeLte) !== 400
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'withRuntimeLte',
max !== 400 && Number(currentFilters.withRuntimeGte) !== 0
? max.toString()
: undefined
);
}}
defaultMaxValue={
currentFilters.withRuntimeLte
? Number(currentFilters.withRuntimeLte)
: undefined
}
defaultMinValue={
currentFilters.withRuntimeGte
? Number(currentFilters.withRuntimeGte)
: undefined
}
subText={intl.formatMessage(messages.runtimeText, {
minValue: currentFilters.withRuntimeGte ?? 0,
maxValue: currentFilters.withRuntimeLte ?? 400,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.tmdbuserscore)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={1}
max={10}
defaultMaxValue={
currentFilters.voteAverageLte
? Number(currentFilters.voteAverageLte)
: undefined
}
defaultMinValue={
currentFilters.voteAverageGte
? Number(currentFilters.voteAverageGte)
: undefined
}
onUpdateMin={(min) => {
updateQueryParams(
'voteAverageGte',
min !== 1 && Number(currentFilters.voteAverageLte) !== 10
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'voteAverageLte',
max !== 10 && Number(currentFilters.voteAverageGte) !== 1
? max.toString()
: undefined
);
}}
subText={intl.formatMessage(messages.ratingText, {
minValue: currentFilters.voteAverageGte ?? 1,
maxValue: currentFilters.voteAverageLte ?? 10,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.tmdbuservotecount)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={0}
max={1000}
defaultMaxValue={
currentFilters.voteCountLte
? Number(currentFilters.voteCountLte)
: undefined
}
defaultMinValue={
currentFilters.voteCountGte
? Number(currentFilters.voteCountGte)
: undefined
}
onUpdateMin={(min) => {
updateQueryParams(
'voteCountGte',
min !== 0 && Number(currentFilters.voteCountLte) !== 1000
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'voteCountLte',
max !== 1000 && Number(currentFilters.voteCountGte) !== 0
? max.toString()
: undefined
);
}}
subText={intl.formatMessage(messages.voteCount, {
minValue: currentFilters.voteCountGte ?? 0,
maxValue: currentFilters.voteCountLte ?? 1000,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.streamingservices)}
</span>
{type in ['movie', 'tv']
?(<WatchProviderSelector
type={type as 'movie' | 'tv'}
region={currentFilters.watchRegion}
activeProviders={
currentFilters.watchProviders?.split('|').map((v) => Number(v)) ??
[]
}
onChange={(region, providers) => {
if (providers.length) {
batchUpdateQueryParams({
watchRegion: region,
watchProviders: providers.join('|'),
});
} else {
batchUpdateQueryParams({
watchRegion: undefined,
watchProviders: undefined,
});
}
{type !== 'music' && (
<>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.originalLanguage)}
</span>
<LanguageSelector
value={currentFilters.language}
serverValue={currentSettings.originalLanguage}
isUserSettings
setFieldValue={(_key, value) => {
updateQueryParams('language', value);
}}
/>)
: null}
</>)
}
/>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.runtime)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={0}
max={400}
onUpdateMin={(min) => {
updateQueryParams(
'withRuntimeGte',
min !== 0 && Number(currentFilters.withRuntimeLte) !== 400
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'withRuntimeLte',
max !== 400 && Number(currentFilters.withRuntimeGte) !== 0
? max.toString()
: undefined
);
}}
defaultMaxValue={
currentFilters.withRuntimeLte
? Number(currentFilters.withRuntimeLte)
: undefined
}
defaultMinValue={
currentFilters.withRuntimeGte
? Number(currentFilters.withRuntimeGte)
: undefined
}
subText={intl.formatMessage(messages.runtimeText, {
minValue: currentFilters.withRuntimeGte ?? 0,
maxValue: currentFilters.withRuntimeLte ?? 400,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.tmdbuserscore)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={1}
max={10}
defaultMaxValue={
currentFilters.voteAverageLte
? Number(currentFilters.voteAverageLte)
: undefined
}
defaultMinValue={
currentFilters.voteAverageGte
? Number(currentFilters.voteAverageGte)
: undefined
}
onUpdateMin={(min) => {
updateQueryParams(
'voteAverageGte',
min !== 1 && Number(currentFilters.voteAverageLte) !== 10
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'voteAverageLte',
max !== 10 && Number(currentFilters.voteAverageGte) !== 1
? max.toString()
: undefined
);
}}
subText={intl.formatMessage(messages.ratingText, {
minValue: currentFilters.voteAverageGte ?? 1,
maxValue: currentFilters.voteAverageLte ?? 10,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.tmdbuservotecount)}
</span>
<div className="relative z-0">
<MultiRangeSlider
min={0}
max={1000}
defaultMaxValue={
currentFilters.voteCountLte
? Number(currentFilters.voteCountLte)
: undefined
}
defaultMinValue={
currentFilters.voteCountGte
? Number(currentFilters.voteCountGte)
: undefined
}
onUpdateMin={(min) => {
updateQueryParams(
'voteCountGte',
min !== 0 && Number(currentFilters.voteCountLte) !== 1000
? min.toString()
: undefined
);
}}
onUpdateMax={(max) => {
updateQueryParams(
'voteCountLte',
max !== 1000 && Number(currentFilters.voteCountGte) !== 0
? max.toString()
: undefined
);
}}
subText={intl.formatMessage(messages.voteCount, {
minValue: currentFilters.voteCountGte ?? 0,
maxValue: currentFilters.voteCountLte ?? 1000,
})}
/>
</div>
<span className="text-lg font-semibold">
{intl.formatMessage(messages.streamingservices)}
</span>
{type in ['movie', 'tv'] ? (
<WatchProviderSelector
type={type as 'movie' | 'tv'}
region={currentFilters.watchRegion}
activeProviders={
currentFilters.watchProviders
?.split('|')
.map((v) => Number(v)) ?? []
}
onChange={(region, providers) => {
if (providers.length) {
batchUpdateQueryParams({
watchRegion: region,
watchProviders: providers.join('|'),
});
} else {
batchUpdateQueryParams({
watchRegion: undefined,
watchProviders: undefined,
});
}
}}
/>
) : null}
</>
)}
<div className="pt-4">
<Button
className="w-full"

@ -43,12 +43,14 @@ type SingleVal = {
type BaseSelectorMultiProps = {
defaultValue?: string;
isMulti: true;
type?: 'movie' | 'tv' | 'music';
onChange: (value: MultiValue<SingleVal> | null) => void;
};
type BaseSelectorSingleProps = {
defaultValue?: string;
isMulti?: false;
type?: 'movie' | 'tv' | 'music';
onChange: (value: SingleValue<SingleVal> | null) => void;
};
@ -206,6 +208,7 @@ export const GenreSelector = ({
export const KeywordSelector = ({
isMulti,
defaultValue,
type,
onChange,
}: BaseSelectorMultiProps | BaseSelectorSingleProps) => {
const intl = useIntl();
@ -219,41 +222,53 @@ export const KeywordSelector = ({
return;
}
const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return keyword.data;
})
);
setDefaultDataValue(
keywords.map((keyword) => ({
label: keyword.name,
value: keyword.id,
}))
);
if (type !== 'music') {
const keywords = await Promise.all(
defaultValue.split(',').map(async (keywordId) => {
const keyword = await axios.get<Keyword>(
`/api/v1/keyword/${keywordId}`
);
return keyword.data;
})
);
setDefaultDataValue(
keywords.map((keyword) => ({
label: keyword.name,
value: keyword.id,
}))
);
}
};
loadDefaultKeywords();
}, [defaultValue]);
}, [defaultValue, type]);
const loadKeywordOptions = async (inputValue: string) => {
const results = await axios.get<TmdbKeywordSearchResponse>(
const results = await axios.get<TmdbKeywordSearchResponse | string[]>(
'/api/v1/search/keyword',
{
params: {
query: encodeURIExtraParams(inputValue),
type,
},
}
);
return results.data.results.map((result) => ({
label: result.name,
value: result.id,
}));
if (type === 'music') {
return (results.data as string[]).map((result, idx) => ({
label: result,
value: idx,
}));
} else {
return (results.data as TmdbKeywordSearchResponse).results.map(
(result) => ({
label: result.name,
value: result.id,
})
);
}
};
return (

Loading…
Cancel
Save