Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
7.1 kB · 202 lines
TSX
at playEdit
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202import { useListCollection, Stack, Text, HStack } from "@chakra-ui/react"import { useDebouncedState } from '@tanstack/react-pacer'import { useQuery } from '@tanstack/react-query';import { tanQueries } from "../../queries/index.js";import type { TrackSearchResult } from "../../../core/Api.js";import React, { useCallback, useEffect, useState } from "react";import { ArtistCreditTags } from "../ArtistCreditDisplay.js";import { LeftSideMetadataResultContent, MetadataPickMenu, MetadataSearchCombobox, type MetadataPartials } from "./MetadataResults.js";import { type ArtMeta, type PlayObjectMinimal } from "../../../core/Atomic.js";import { removeUndefinedKeys } from "../../../core/DataUtils.js";import { MusicServiceIndicators } from "../musicServices/MusicServiceIndicators.js";import { albumSearchResultToMusicServices } from "./AlbumSearch.js";import type { MusicServicesAny } from "../musicServices/musicServiceTypes.js";
const trackPartials: MetadataPartials<TrackSearchResult> = { track: { label: 'Track only', pick: ({ artists, album, albumCount, ...rest }) => rest }, artists: { label: 'Track + artists', pick: ({ album, albumCount, ...rest }) => rest }, album: { label: 'Track + album', pick: ({ artists, ...rest }) => rest },};
export const trackSearchResultToMusicServices = (val: MinimalResult): MusicServicesAny[] => { const musicServices: MusicServicesAny[] = []; if (val.mbidTrack) { musicServices.push({ name: 'musicbrainz', id: val.mbidTrack, idHint: 'track' }); } else if (val.mbidRecording) { musicServices.push({ name: 'musicbrainz', id: val.mbidRecording, idHint: 'recording' }); } if(val.spotifyId !== undefined) { musicServices.push({ name: 'spotify', id: val.spotifyId, idHint: 'track' }); } return musicServices;}
export const TrackSearchResultItem = (props: { data: TrackSearchResult, onPick?: (val: TrackSearchResult) => void }) => {
const { onPick } = props;
const { album, albumCount, name, artists = [] } = props.data;
let albumContent: React.JSX.Element | undefined = undefined; if (album !== undefined) { const andCount = albumCount !== undefined && albumCount > 1 ? ( <Text color="fg.subtle" textStyle="sm"> and {albumCount} more... </Text> ) : undefined; albumContent = (<Text color="fg.muted" textStyle="sm"> <HStack> {album.name} {album.type !== undefined ? `(${album.type})` : ''}<MusicServiceIndicators services={albumSearchResultToMusicServices(album)}/> {andCount} </HStack> </Text>) }
let artistTags: React.JSX.Element | undefined = undefined; if (artists.length > 0) { artistTags = <ArtistCreditTags data={artists} /> }
return ( <HStack gap="4" flexGrow="1"> <LeftSideMetadataResultContent {...props.data} /> <Stack gap="1" flexGrow="1"> <Text fontWeight="medium" mb="1"> <HStack> {name} <MusicServiceIndicators services={trackSearchResultToMusicServices(props.data)}/> <MetadataPickMenu data={props.data} partials={trackPartials} onPick={onPick} /> </HStack> </Text> {artistTags} {albumContent} </Stack> </HStack> )}
type MinimalResult = Pick<TrackSearchResult, 'name' | 'mbidTrack' | 'mbidRecording' | 'spotifyId'>export interface TrackSearchProps { initial?: PlayObjectMinimal<string> onChange: (val: PlayObjectMinimal<string>) => void}
const trackResultToPlay = (val: TrackSearchResult): PlayObjectMinimal<string> => { const { name, mbidTrack, mbidRecording, spotifyId, artists, album, image } = val;
const play = removeUndefinedKeys<PlayObjectMinimal<string>>({ data: { track: name, artists: artists !== undefined && artists.length > 0 ? artists.map((x) => ({name: x.name, mbid: x.mbid, spotifyId: x.spotifyId})) : undefined, album: album?.name, meta: removeUndefinedKeys({ brainz: removeUndefinedKeys({ track: mbidTrack, recording: mbidRecording, album: album?.mbidRelease, releaseGroup: album?.mbidReleaseGroup }), spotify: removeUndefinedKeys({ track: spotifyId }) }) }, meta: {} }, false);
const art = removeUndefinedKeys<ArtMeta>({ album: album?.image, track: image, artist: (artists ?? []).find(x => x.image !== undefined)?.image }); if(art !== undefined) { play.meta = { art } }
return play;}
export const TrackSearch = (props: TrackSearchProps) => {
const { initial, onChange = (val) => console.log(val, 'Selected value for prop') } = props;
const { data: { track, meta: { brainz: { recording, track: mbidTrack } = {}, spotify: { track: spotifyId } = {} } = {} } = {}, } = initial ?? {};
const [selectedItem, setSelectedItem] = useState<MinimalResult>(removeUndefinedKeys({ name: track ?? '', mbidTrack, mbidRecording: recording, spotifyId }, false)); const [debouncedQuery, setDebouncedQuery] = useDebouncedState<string>(track ?? '', { wait: 500 });
const query = useQuery({ enabled: debouncedQuery !== '', ...tanQueries.metadata.track(debouncedQuery) });
const { collection, set } = useListCollection<TrackSearchResult>({ initialItems: query.data?.data ?? [], itemToString: (item) => item.name, itemToValue: (item) => item.id, });
useEffect(() => { if (query.isSuccess) { set(query.data.data); } }, [query, set])
const doChange = useCallback((val: TrackSearchResult) => { setSelectedItem(val); onChange(trackResultToPlay(val)); },[setSelectedItem, onChange]);
const services = trackSearchResultToMusicServices(selectedItem); let groupContent: React.JSX.Element | undefined = undefined; if(services.length > 0) { groupContent = <MusicServiceIndicators services={services} link={false}/> }
return ( <MetadataSearchCombobox placeholder="Type to search for tracks" collection={collection} inputGroupContent={groupContent} isLoading={query.isLoading} isError={query.isError} initialInput={selectedItem?.name} onChange={doChange} onQueryChange={setDebouncedQuery} renderItem={(item, onPick) => <TrackSearchResultItem data={item} onPick={onPick} />} /> );}