diff --git a/.env b/.env index b64fd2e..f015dbd 100644 --- a/.env +++ b/.env @@ -8,6 +8,7 @@ SQLITE_DB_PATH="/var/www/html/database/development.db" # Optional DISCOGS_API_KEY= DISCOGS_SECRET= +SETLISTFM_API_KEY= REACT_APP_ANALYTICS_MEASUREMENT_ID= REACT_APP_GROWTHBOOK_API_KEY= REACT_APP_LINK_TO_TRANSLATIONS="https://github.com/elamperti/OpenWebScrobbler/blob/main/CONTRIBUTING.md#translations" diff --git a/README.md b/README.md index 1af4d07..978951d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ To join the translators team, follow the link at the bottom of the language sele * Clone the repository * Copy `.env` to `.env.development.local`, then set at least the required variables * You'll need a [Last.fm API account](https://www.last.fm/api/account/create) to be able to interact with Open Scrobbler (it's used for authentication and queries). Once you have your keys, fill in `REACT_APP_LASTFM_API_KEY` and `LASTFM_API_KEY` (same value in both) and `LASTFM_SECRET` - * Optional: to interact with Discogs, create a [Discogs application](https://www.discogs.com/settings/developers) to get API keys and fill in `DISCOGS_API_KEY` and `DISCOGS_SECRET` + * Optional: to interact with Discogs, create a [Discogs application](https://www.discogs.com/settings/developers) to get API keys and fill in `DISCOGS_API_KEY` and `DISCOGS_SECRET` + * Optional: to interact with Setlist.fm, [create an application](https://www.setlist.fm/settings/apps) and set `SETLISTFM_API_KEY` with the corresponding API key * Run `yarn` to download the required libraries. * Run `yarn start` to initialize the docker container and run the application. diff --git a/public/api/v2/setlistfm.php b/public/api/v2/setlistfm.php index 43dd884..8fcef74 100644 --- a/public/api/v2/setlistfm.php +++ b/public/api/v2/setlistfm.php @@ -1,29 +1,38 @@ timing('Setlist.fm Response Time', $method, round(curl_getinfo($setlistfmrq, CURLINFO_TOTAL_TIME) * 1000)); curl_close($setlistfmrq); echo $response; - } else { - require('inc/error.php'); - raiseOWSError('No setlist ID provided', 404, 602); - } + diff --git a/src/Constants.ts b/src/Constants.ts index 8a39b7b..3a1d1c1 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -6,7 +6,6 @@ export const LASTFM_AUTH_URL = `&cb=${window.location.protocol}//${window.location.host}/lastfm/callback/`; export const PATREON_AUTH_URL = `https://www.patreon.com/oauth2/authorize?response_type=code&client_id=${process.env.REACT_APP_PATREON_CLIENT_ID}&redirect_uri=${window.location.protocol}//${process.env.REACT_APP_HOST}/patreon/callback`; export const OPENSCROBBLER_API_URL = '/api/v2'; -export const SETLISTFM_API_URL = '/api/v2/setlistfm.php'; export const CONSIDER_HISTORY_STALE_AFTER = 5 * 60 * 1000; // 5 minutes export const SETTINGS_DEBOUNCE_PERIOD = 3 * 1000; // 3 seconds export const SCROBBLING_DEBOUNCE_PERIOD = 1.5 * 1000; // 1.5 seconds @@ -17,7 +16,7 @@ export const MAX_RECENT_ALBUMS = 8; export const DEFAULT_SONG_DURATION = 3 * 60; // ToDo: use this value when skipping time forward after scrobble // Live music is unpredictable, but usually the band takes a few seconds to breathe / introduce song. // Adding some leeway to encapsulate this, but it will never be perfect. -export const DEFAULT_CONCERT_SONG_BUFFER = 60; +export const DEFAULT_CONCERT_INTERVAL_DURATION = 2 * 60; // ToDo: improve this export type Provider = 'lastfm' | 'discogs' | 'spotify' | 'setlistfm'; diff --git a/src/Routes.tsx b/src/Routes.tsx index 29cc465..fdfb894 100644 --- a/src/Routes.tsx +++ b/src/Routes.tsx @@ -35,7 +35,7 @@ export default function Routes() { } /> } /> - } /> + } /> } /> } /> diff --git a/src/domains/home/HomeUser.tsx b/src/domains/home/HomeUser.tsx index c4636ea..608ec7f 100644 --- a/src/domains/home/HomeUser.tsx +++ b/src/domains/home/HomeUser.tsx @@ -1,5 +1,5 @@ import { Row } from 'reactstrap'; -import { faCompactDisc, faPencilAlt, faUserFriends, faList } from '@fortawesome/free-solid-svg-icons'; +import { faCompactDisc, faPencilAlt, faUserFriends } from '@fortawesome/free-solid-svg-icons'; import BigHomeButton from './partials/BigHomeButton'; import SocialNetworksBlock from './partials/SocialNetworksBlock'; import WelcomeBlock from './partials/WelcomeBlock'; @@ -13,7 +13,6 @@ export default function Home() { - diff --git a/src/domains/scrobbleSetlist/ScrobbleSetlistResult.tsx b/src/domains/scrobbleSetlist/ScrobbleSetlistResult.tsx index 7b0f07c..a58ff11 100644 --- a/src/domains/scrobbleSetlist/ScrobbleSetlistResult.tsx +++ b/src/domains/scrobbleSetlist/ScrobbleSetlistResult.tsx @@ -2,40 +2,48 @@ import { faList } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; -import { setlistSearch as SetlistFmSearch } from 'utils/clients/setlistfm'; +import { searchSetlist } from 'utils/clients/setlistfm'; +import { SetlistList } from './partials/SetlistList'; +import Paginator from 'components/Paginator'; +import Spinner from 'components/Spinner'; +import { Trans } from 'react-i18next'; export function ScrobbleSetlistResult() { const params = useParams(); - const navigate = useNavigate(); - const [query, setQuery] = useState(''); + const [artistName, setArtistName] = useState(''); + const [currentPage, setCurrentPage] = useState(1); // This extracts the search parameter from the URL useEffect(() => { - setQuery(decodeURIComponent(params?.setlistId || '')); + setArtistName(decodeURIComponent(params?.query || '').toLowerCase()); }, [params]); - const { data } = useQuery({ - queryKey: ['setlist', query], + const { data, isLoading } = useQuery({ + queryKey: ['setlist', 'search', 'artist', artistName, currentPage], queryFn: () => { - return SetlistFmSearch(query); + return searchSetlist(artistName, currentPage); }, - enabled: !!query, + enabled: !!artistName, }); - useEffect(() => { - if (data !== undefined) { - navigate(`/scrobble/setlist/view/${data.id}`, { state: data }); - } - }, [data, navigate]); - return ( <>

- Scrobble Setlist Result + Scrobble Setlist

+ {isLoading ? ( + + ) : ( + <> + {data?.results && } + {data?.totalPages > 1 && ( + + )} + + )} ); } diff --git a/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.test.ts b/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.test.ts index 27b6816..b54f402 100644 --- a/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.test.ts +++ b/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.test.ts @@ -12,4 +12,10 @@ describe('`extractSetlistID` helper', () => { const result = extractSetlistID(text); expect(result).toEqual('ba97d62'); }); + + it('fails to get an ID for a random text', () => { + const text = 'Lorem ipsum'; + const result = extractSetlistID(text); + expect(result).toEqual(null); + }); }); diff --git a/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.tsx b/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.tsx index 7b6aa8f..5984c0a 100644 --- a/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.tsx +++ b/src/domains/scrobbleSetlist/ScrobbleSetlistSearch.tsx @@ -1,19 +1,16 @@ import ReactGA from 'react-ga-neo'; import SearchForm from 'components/SearchForm'; -import { PROVIDER_SETLISTFM, PROVIDER_NAME } from 'Constants'; import { useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; -export function extractSetlistID(setlistUrl: string) { - const setlistId = setlistUrl.trim().match(/([a-zA-Z0-9]+)(?=\.html)/)[0]; - return setlistId; +export function extractSetlistID(setlistUrl: string): string | null { + return setlistUrl.match(/([a-zA-Z0-9]+)(?=\.htm)/)?.[0] ?? null; } export function ScrobbleSetlistSearch() { const navigate = useNavigate(); - const validator = (setlistUrl) => - setlistUrl.trim().length > 0 && setlistUrl.includes('setlist.fm') && setlistUrl.includes('.html'); + const validator = (setlistUrl) => setlistUrl.trim().length > 2; const { t } = useTranslation(); const onSearch = (query) => { @@ -23,24 +20,25 @@ export function ScrobbleSetlistSearch() { }); const setlistId = extractSetlistID(query); - navigate(`/scrobble/setlist/search/${setlistId}`, { - state: { - query, - }, - }); + if (setlistId) { + navigate(`/scrobble/setlist/view/${setlistId}`); + } else { + navigate(`/scrobble/setlist/search/${query}`, { + state: { + query, + }, + }); + } }; return (

Scrobble Setlist

- Please provide the setlist link from{' '} - - Setlilst.fm - + Search by artist name or paste a setlist.fm link

state.scrobbles.list); - const setlist = setlistTransformer(state); + + // This extracts the search parameter from the URL + useEffect(() => { + setSetlistId(decodeURIComponent(params?.setlistId || '').toLowerCase()); + }, [params]); + + const { data, isLoading } = useQuery({ + queryKey: ['setlist', 'view', setlistId], + queryFn: () => { + return getSetlistById(setlistId); + }, + enabled: !!setlistId, + }); + return ( <>

- Scrobble Setlist + Scrobble Setlist

-
- -
+
{isLoading || !data ? : }

diff --git a/src/domains/scrobbleSetlist/partials/EmptySetlistMessage.tsx b/src/domains/scrobbleSetlist/partials/EmptySetlistMessage.tsx new file mode 100644 index 0000000..8a5b5cd --- /dev/null +++ b/src/domains/scrobbleSetlist/partials/EmptySetlistMessage.tsx @@ -0,0 +1,27 @@ +import { useNavigate } from 'react-router-dom'; +import { Trans } from 'react-i18next'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBolt, faArrowLeft, faGuitar } from '@fortawesome/free-solid-svg-icons'; + +export function EmptySetlistMessage() { + const navigate = useNavigate(); + + const goBack = (e) => { + e.preventDefault(); + navigate(-1); + }; + + return ( +
+
+ +

+ This setlist seems to be empty. +

+ + Go back + +
+
+ ); +} diff --git a/src/domains/scrobbleSetlist/partials/SetlistCard.tsx b/src/domains/scrobbleSetlist/partials/SetlistCard.tsx new file mode 100644 index 0000000..e7b046f --- /dev/null +++ b/src/domains/scrobbleSetlist/partials/SetlistCard.tsx @@ -0,0 +1,60 @@ +import { faMapMarkerAlt, faUser } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import { Card, CardBody } from 'reactstrap'; +import type { Setlist } from 'utils/types/setlist'; + +export function SetlistCard({ + setlist, + className = '', + linkArtist = false, +}: { + setlist: Setlist; + className?: string; + linkArtist?: boolean; +}) { + const { t } = useTranslation(); + const shortMonth = t('dates.months.short', { returnObjects: true })[setlist.date.getMonth()]; + + const ArtistInfo = ( + <> + + {setlist.artist} + + ); + + return ( + + +
+ + {shortMonth} + + {setlist.date.getDate()} + {setlist.date.getFullYear()} +
+
+

{setlist.venue.name}

+ + {linkArtist ? ( + + {ArtistInfo} + + ) : ( + ArtistInfo + )} + + + + {setlist.venue.city} + {setlist.venue.state ? `, ${setlist.venue.state}` : ''} ({setlist.venue.country}) + +
+
+
+ ); +} diff --git a/src/domains/scrobbleSetlist/partials/SetlistList.tsx b/src/domains/scrobbleSetlist/partials/SetlistList.tsx new file mode 100644 index 0000000..b2e3cb6 --- /dev/null +++ b/src/domains/scrobbleSetlist/partials/SetlistList.tsx @@ -0,0 +1,28 @@ +import { Trans } from 'react-i18next'; +import { SetlistCard } from './SetlistCard'; +import type { Setlist } from 'utils/types/setlist'; +import { Link } from 'react-router-dom'; + +export function SetlistList({ setlists, query }: { setlists: Setlist[]; query: string }) { + if (!Array.isArray(setlists) || setlists.length === 0) { + return ( +
+ + No setlists found for your search query + +
+ ); + } + + return ( +
+
+ {setlists.map((setlist) => ( + + + + ))} +
+
+ ); +} diff --git a/src/domains/scrobbleSetlist/Setlist.tsx b/src/domains/scrobbleSetlist/partials/SetlistViewer.tsx similarity index 55% rename from src/domains/scrobbleSetlist/Setlist.tsx rename to src/domains/scrobbleSetlist/partials/SetlistViewer.tsx index bf2afc9..a7b46f4 100644 --- a/src/domains/scrobbleSetlist/Setlist.tsx +++ b/src/domains/scrobbleSetlist/partials/SetlistViewer.tsx @@ -1,26 +1,29 @@ import ScrobbleList from 'components/ScrobbleList'; import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; import { addSeconds } from 'date-fns'; - import lazyWithPreload from 'react-lazy-with-preload'; -import type { SetTrack, Setlist } from 'utils/types/setlist'; -import { Track } from 'utils/types/track'; -import EmptyScrobbleListFiller from 'components/EmptyScrobbleListFiller'; import { Button } from 'reactstrap'; -import { SetlistFmArtist } from 'utils/types/artist'; import { Trans } from 'react-i18next'; -import { DEFAULT_CONCERT_SONG_BUFFER, DEFAULT_SONG_DURATION } from 'Constants'; + +import { DEFAULT_CONCERT_INTERVAL_DURATION, DEFAULT_SONG_DURATION } from 'Constants'; import { enqueueScrobble } from 'store/actions/scrobbleActions'; -import { useDispatch } from 'react-redux'; -import { Scrobble } from 'utils/types/scrobble'; + +import type { Scrobble } from 'utils/types/scrobble'; +import type { Setlist } from 'utils/types/setlist'; +import { SetlistCard } from './SetlistCard'; +import { EmptySetlistMessage } from './EmptySetlistMessage'; const DateTimePicker = lazyWithPreload(() => import('components/DateTimePicker')); export default function SetlistViewer({ setlist }: { setlist: Setlist | null }) { const dispatch = useDispatch(); const [selectedTracks, setSelectedTracks] = useState>(new Set()); - const [allTracks, setAllTracks] = useState([]); - const [customTimestamp, setCustomTimestamp] = useState(setlist.eventDate); + const [customTimestamp, setCustomTimestamp] = useState(setlist.date); + const [hasBeenScrobbled, setSetlistScrobbled] = useState(false); + const setlistIsValid = setlist.trackCount > 0; + + const tracks = setlist.tracks || []; DateTimePicker.preload(); @@ -36,33 +39,14 @@ export default function SetlistViewer({ setlist }: { setlist: Setlist | null }) setSelectedTracks(newSet); }; - function getAllTracks() { - const fullSongList: Track[] = []; - const idSet: Set = new Set(); - const artist = setlist.artist; - let counter = 0; - for (const set of setlist.sets) { - for (const song of set.songs) { - if (song.tape) { - continue; - } - fullSongList.push(songToTrack({ song, concertArtist: artist, num: counter })); - idSet.add(counter.toString()); - counter++; - } - fullSongList.concat(); - } - return { fullSongList, idSet }; - } - const handleTimestampChange = (newTimestamp) => { setCustomTimestamp(newTimestamp); }; const scrobbleSelectedTracks = () => { - const usingCustomSelection = selectedTracks.size < 1 && selectedTracks.size === allTracks.length; + const usingCustomSelection = selectedTracks.size < 1 && selectedTracks.size === tracks.length; let rollingTimestamp = customTimestamp; - const tracksToScrobble = allTracks + const tracksToScrobble = tracks .filter(({ id }) => usingCustomSelection || selectedTracks.has(id)) .reduce((result, track) => { const newTrack = { @@ -73,47 +57,25 @@ export default function SetlistViewer({ setlist }: { setlist: Setlist | null }) // Prepare timestamp for next track. rollingTimestamp = addSeconds( rollingTimestamp, - (track.duration || DEFAULT_SONG_DURATION) + DEFAULT_CONCERT_SONG_BUFFER + (track.duration || DEFAULT_SONG_DURATION) + DEFAULT_CONCERT_INTERVAL_DURATION ); result.push(newTrack); return result; }, []); enqueueScrobble(dispatch)(tracksToScrobble); + setSetlistScrobbled(true); setSelectedTracks(new Set()); }; useEffect(() => { - const { fullSongList, idSet } = getAllTracks(); - setAllTracks(fullSongList); - setSelectedTracks(idSet); + setSelectedTracks(new Set(setlist.tracks?.map(({ id }) => id) || [])); }, [setlist]); - function songToTrack({ song, concertArtist, num }: { song: SetTrack; concertArtist: SetlistFmArtist; num: number }) { - let ourArtist = concertArtist.name; - if ('originalArtist' in song) { - ourArtist = song.originalArtist.name; - } - return { - id: num.toString(), - artist: ourArtist, - trackNumber: num, - title: song.name, - album: null, - albumArtist: ourArtist, - duration: null, - } as Track; - } - return ( <> - {true && ( -
-

{setlist.tour}

-

{setlist.artist.name}

-
- )} - + + {setlistIsValid && }
@@ -121,13 +83,11 @@ export default function SetlistViewer({ setlist }: { setlist: Setlist | null }) className="w-100 me-3" color="success" onClick={scrobbleSelectedTracks} - disabled={allTracks.length < 1} + disabled={hasBeenScrobbled || !setlistIsValid} > 0 && selectedTracks.size < allTracks.length - ? 'scrobbleSelected' - : 'scrobbleSetlist' + selectedTracks.size > 0 && selectedTracks.size < tracks.length ? 'scrobbleSelected' : 'scrobbleSetlist' } > Scrobble setlist @@ -141,12 +101,12 @@ export default function SetlistViewer({ setlist }: { setlist: Setlist | null }) isAlbum={true} noMenu analyticsEventForScrobbles="Scrobble individual setlist song" - scrobbles={allTracks || []} + scrobbles={tracks || []} albumHasVariousArtists={true} onSelect={toggleSelectTrack} selected={selectedTracks} > - + ); diff --git a/src/store/actions/setlistActions.js b/src/store/actions/setlistActions.js index 006df38..881a6f1 100644 --- a/src/store/actions/setlistActions.js +++ b/src/store/actions/setlistActions.js @@ -7,7 +7,7 @@ export async function _setlistfmFindSetlist(setlistId) { }, }); - if (data.setlist.sets && data.setlist.sets.length() > 0) { - return data.setlist.sets[0].set; + if (data.setlist.tracks && data.setlist.tracks.length() > 0) { + return data.setlist.tracks[0].set; } } diff --git a/src/utils/clients/setlistfm/apiClient.ts b/src/utils/clients/setlistfm/apiClient.ts index b614959..6d9ee53 100644 --- a/src/utils/clients/setlistfm/apiClient.ts +++ b/src/utils/clients/setlistfm/apiClient.ts @@ -1,11 +1,7 @@ import axios from 'axios'; -import { SETLISTFM_API_URL } from 'Constants'; +import { OPENSCROBBLER_API_URL } from 'Constants'; export const setlistfmAPI = axios.create({ - baseURL: SETLISTFM_API_URL, - params: { - api_key: process.env.SETLISTFM_API_KEY, - format: 'json', - }, + baseURL: `${OPENSCROBBLER_API_URL}/setlistfm.php`, adapter: undefined, }); diff --git a/src/utils/clients/setlistfm/index.ts b/src/utils/clients/setlistfm/index.ts index 602bda9..e38b270 100644 --- a/src/utils/clients/setlistfm/index.ts +++ b/src/utils/clients/setlistfm/index.ts @@ -1 +1,2 @@ -export { setlistSearch } from './methods/setlistSearch'; +export { getSetlistById } from './methods/setlistGetById'; +export { searchSetlist } from './methods/setlistSearchByArtist'; diff --git a/src/utils/clients/setlistfm/methods/setlistGetById.ts b/src/utils/clients/setlistfm/methods/setlistGetById.ts new file mode 100644 index 0000000..d835fe8 --- /dev/null +++ b/src/utils/clients/setlistfm/methods/setlistGetById.ts @@ -0,0 +1,12 @@ +import { setlistfmAPI } from 'utils/clients/setlistfm/apiClient'; +import { setlistTransformer } from '../transformers/setlist.transformer'; + +export async function getSetlistById(setlistId) { + const { data } = await setlistfmAPI.get('', { + params: { + setlistId, + }, + }); + + return setlistTransformer(data, true); +} diff --git a/src/utils/clients/setlistfm/methods/setlistSearch.ts b/src/utils/clients/setlistfm/methods/setlistSearch.ts deleted file mode 100644 index 3ff90ed..0000000 --- a/src/utils/clients/setlistfm/methods/setlistSearch.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { setlistfmAPI } from 'utils/clients/setlistfm/apiClient'; - -export async function setlistSearch(setlistId) { - const { data } = await setlistfmAPI.get('', { - params: { - setlist_id: setlistId, - }, - }); - return data; -} diff --git a/src/utils/clients/setlistfm/methods/setlistSearchByArtist.ts b/src/utils/clients/setlistfm/methods/setlistSearchByArtist.ts new file mode 100644 index 0000000..5db0fb7 --- /dev/null +++ b/src/utils/clients/setlistfm/methods/setlistSearchByArtist.ts @@ -0,0 +1,12 @@ +import { setlistfmAPI } from 'utils/clients/setlistfm/apiClient'; +import { setlistSearchTransformer } from '../transformers/setlistSearch.transformer'; + +export async function searchSetlist(artistName, page) { + const { data } = await setlistfmAPI.get('', { + params: { + artistName, + page: page > 1 ? page : undefined, + }, + }); + return setlistSearchTransformer(data); +} diff --git a/src/utils/clients/setlistfm/transformers.ts/setlist.transformer.ts b/src/utils/clients/setlistfm/transformers.ts/setlist.transformer.ts deleted file mode 100644 index e8f4758..0000000 --- a/src/utils/clients/setlistfm/transformers.ts/setlist.transformer.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { MusicalSet, SetTrack, Setlist, Venue } from 'utils/types/setlist'; -import { SetlistFmArtist } from 'utils/types/artist'; -import { parse } from 'date-fns'; - -export function setlistTransformer(raw: any) { - const dateString = raw?.eventDate || null; - let date = null; - if (dateString) { - date = parse(dateString, 'dd-MM-yyyy', new Date()); - } - return { - id: raw?.id || null, - versionId: raw?.versionId || null, - eventDate: date, - artist: setlistArtistTransformer(raw?.artist), - venue: setlistVenueTransformer(raw?.venue), - tour: raw?.tour?.name || null, - sets: setlistSetsTransformer(raw?.sets?.set), - url: null, - } as Setlist; -} - -function setlistArtistTransformer(artist: any): SetlistFmArtist { - return { - name: artist?.name || artist?.sortName || '', - mbid: artist?.mbid || null, - url: artist?.url || '', - } as SetlistFmArtist; -} - -function setlistSetsTransformer(sets: any[]): MusicalSet[] { - const listOfSets: MusicalSet[] = []; - let counter = 1; - for (const set of sets) { - const newSet = { - name: set.name || `Set #${counter}`, - songs: setlistSongsTransformer(set.song), - } as MusicalSet; - counter = counter++; - listOfSets.push(newSet); - } - return listOfSets; -} - -function setlistVenueTransformer(venue: any): Venue { - const city = venue?.city; - return { - id: venue?.id, - name: venue?.name || '', - city: city?.name || null, - state: city?.state || null, - country: city?.country?.name || null, - } as Venue; -} - -function setlistSongsTransformer(songs: any[]): SetTrack[] { - const listOfSongs: SetTrack[] = []; - for (const song of songs) { - const songTitle = song.name; - const tape = song.tape ?? false; - let setTrack = { name: songTitle, tape } as SetTrack; - if ('cover' in song) { - setTrack = { - name: songTitle, - tape, - originalArtist: { - name: song.cover.name, - mbid: song.cover.mbid, - url: song.cover.url, - } as SetlistFmArtist, - } as SetTrack; - } - listOfSongs.push(setTrack); - } - return listOfSongs; -} diff --git a/src/utils/clients/setlistfm/transformers/setlist.transformer.ts b/src/utils/clients/setlistfm/transformers/setlist.transformer.ts new file mode 100644 index 0000000..d469df1 --- /dev/null +++ b/src/utils/clients/setlistfm/transformers/setlist.transformer.ts @@ -0,0 +1,44 @@ +import { parse } from 'date-fns'; +import shortid from 'shortid'; +import { setlistVenueTransformer } from './venue.transformer'; + +import type { Setlist } from 'utils/types/setlist'; +import type { Track } from 'utils/types/track'; + +export function setlistTransformer(raw: any, withTracks: boolean): Setlist { + const artist = raw?.artist?.name ?? 'Unknown'; + + return { + id: raw.id, + // ToDo: try to do something about the unknown time (use a default? fetch it from elsewhere?) + date: parse(raw.eventDate, 'dd-MM-yyyy', new Date()), + artist, + tour: raw.tour?.name || '', + venue: setlistVenueTransformer(raw?.venue), + tracks: + withTracks && Array.isArray(raw.sets?.set) + ? raw.sets.set.map(({ song: rawSet }: any) => setlistTracklistTransformer(rawSet || [], artist)).flat() + : undefined, + trackCount: Array.isArray(raw.sets?.set) + ? raw.sets.set.reduce((acc: number, { song: rawSet }: any) => acc + (rawSet?.length || 0), 0) + : 0, + url: raw.url || '', + }; +} + +function setlistTracklistTransformer(rawSet: any, artist: string): Track[] { + if (!Array.isArray(rawSet)) return []; + return ( + rawSet + .filter(({ tape }) => Boolean(tape)) + .map((song, i) => ({ + id: shortid.generate(), + trackNumber: i + 1, + artist, + title: song.name, + album: '', + albumArtist: '', + duration: 0, + })) || [] + ); +} diff --git a/src/utils/clients/setlistfm/transformers/setlistSearch.transformer.ts b/src/utils/clients/setlistfm/transformers/setlistSearch.transformer.ts new file mode 100644 index 0000000..8000ba7 --- /dev/null +++ b/src/utils/clients/setlistfm/transformers/setlistSearch.transformer.ts @@ -0,0 +1,13 @@ +import { setlistTransformer } from './setlist.transformer'; + +import type { Setlist } from 'utils/types/setlist'; + +export function setlistSearchTransformer(raw: any) { + const results = raw?.setlist?.map(setlistTransformer); + + return { + page: raw.page ? parseInt(raw.page) : 1, + totalPages: raw.total ? Math.ceil(parseInt(raw.total) / parseInt(raw.itemsPerPage)) : 1, + results: results ?? ([] as Setlist[]), + }; +} diff --git a/src/utils/clients/setlistfm/transformers/venue.transformer.ts b/src/utils/clients/setlistfm/transformers/venue.transformer.ts new file mode 100644 index 0000000..d07cbb9 --- /dev/null +++ b/src/utils/clients/setlistfm/transformers/venue.transformer.ts @@ -0,0 +1,11 @@ +import type { SetlistVenue } from 'utils/types/setlist'; + +export function setlistVenueTransformer(venue: any): SetlistVenue { + const city = venue?.city; + return { + name: venue.name || '', + city: city.name || '', + state: city.state || '', + country: city?.country?.name || '', + }; +} diff --git a/src/utils/types/artist.d.ts b/src/utils/types/artist.d.ts index 20f1047..faa207b 100644 --- a/src/utils/types/artist.d.ts +++ b/src/utils/types/artist.d.ts @@ -14,8 +14,4 @@ export type DiscogsArtist = BaseArtist & { discogsId: string; }; -export type SetlistFmArtist = BaseArtist & { - mbid: string; -}; - -export type Artist = LastFmArtist | DiscogsArtist | SetlistFmArtist; +export type Artist = LastFmArtist | DiscogsArtist; diff --git a/src/utils/types/setlist.d.ts b/src/utils/types/setlist.d.ts index 8635cf0..98e4720 100644 --- a/src/utils/types/setlist.d.ts +++ b/src/utils/types/setlist.d.ts @@ -1,36 +1,17 @@ -import { SetlistFmArtist } from 'utils/types/artist'; - -type Song = { - name: string; - tape: boolean; -}; - -type Cover = Song & { - originalArtist: SetlistFmArtist; -}; - -type SetTrack = Song | Cover; - -type MusicalSet = { - name: string; - songs: SetTrack[]; -}; - -type Venue = { - id: string; +type SetlistVenue = { name: string; city: string; - state: string; + state?: string; country: string; }; export type Setlist = { id: string; - versionId: string; - eventDate: Date; - artist: SetlistFmArtist; + date: Date; + artist: string; tour: string; - venue: Venue; - sets: MusicalSet[]; + venue: SetlistVenue; + trackCount: number; + tracks?: MusicalSet[]; url: string; };