Skip to content

Commit

Permalink
feat/added info playlist (#28)
Browse files Browse the repository at this point in the history
* refactor: improved match

* chore: improved errore handling

* feat: added playlist info
  • Loading branch information
sirLisko authored Oct 7, 2024
1 parent 7c9c591 commit 07dfcf9
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 61 deletions.
4 changes: 2 additions & 2 deletions components/Head/Head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Favicons from "./Favicons";
const HeadSection = () => (
<>
<Head>
<title>GigPlayList - prepare the playlist for your next gig!</title>
<title>GigPlayList - Prepare the playlist for your next gig!</title>
<meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="author" content="Luca Lischetti" />
<link
Expand All @@ -33,7 +33,7 @@ const HeadSection = () => (
/>
<meta
property="og:title"
content="GigPlayList - prepare the playlist for your next gig!"
content="GigPlayList - Prepare the playlist for your next gig!"
/>
<script
defer
Expand Down
112 changes: 103 additions & 9 deletions components/Result/Result.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { ArrowLeft, Frown, TriangleAlert } from "lucide-react";

import Events from "components/Events/Events";
import Tracks from "components/Tracks/Tracks";
Expand All @@ -8,20 +10,65 @@ import SavePlaylist from "components/SavePlaylist/SavePlaylist";
import { useArtistData } from "services/artistData";
import { useTracks } from "services/tracks";
import { useEvents } from "services/events";
import { ArrowLeft, Frown, TriangleAlert } from "lucide-react";
import Link from "next/link";
import { useGetArtist } from "services/searchArtist";
import { matchSongs } from "utils/matchSongs";
import type { Link as LinkType, SetList } from "types";

interface Props {
artistQuery: string[];
}

const sanitiseDate = (dateString: string) => {
if (!dateString) return null;
const [day, month, year] = dateString.split("-");
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
};

const getMinutesAndSeconds = (ms: number) => {
const minutes = Math.floor(ms / 60000); // 60000 ms in one minute
const seconds = Math.floor((ms % 60000) / 1000); // remaining seconds
return { minutes, seconds };
};

const calculatePlaylistDuration = (songs: LinkType[]) => {
if (!songs.length) return 0;
return getMinutesAndSeconds(
songs.reduce((acc, song) => acc + song.duration_ms, 0),
);
};

const generateEncoreLabel = (data: SetList) => {
const totalSetLists = data.totalSetLists;
const encores = data.encores;

if (!encores || !Object.keys(encores).length) return null;

const ordinalSuffix = (n: string) => {
const suffixes = ["th", "st", "nd", "rd"];
const v = parseInt(n) % 100;
return n + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
};

const encoreEntries = Object.entries(encores);

const encoreLabels = encoreEntries.map(([encoreNumber, count]) => {
const probability = ((count / totalSetLists) * 100).toFixed(0);
return `${ordinalSuffix(encoreNumber)} ${probability}%`;
});

return (
<>
<strong>Encore probability</strong>: {encoreLabels.join(", ")}
</>
);
};

const Result = ({ artistQuery }: Props) => {
const [initialBaground] = useState<string>(document.body.style.background);
const { artistData, isLoading: isLoadingArtist } = useArtistData(
artistQuery[0],
);
const { tracks, isLoading: isLoadingTracks } = useTracks(
const { data, isLoading: isLoadingTracks } = useTracks(
artistQuery[0],
artistQuery[1],
);
Expand All @@ -44,7 +91,16 @@ const Result = ({ artistQuery }: Props) => {
return null;
}

const isArtistiWithTrack = tracks && tracks.length > 0 && artistData;
const isArtistiWithTrack =
data?.tracks && data.tracks.length > 0 && artistData;

const songs =
artistData?.tracks && data?.tracks
? matchSongs(data.tracks, artistData.tracks)
: [];

const playlistDuration = calculatePlaylistDuration(songs);
const encoreLabel = data && generateEncoreLabel(data);

return (
<article
Expand Down Expand Up @@ -89,15 +145,14 @@ const Result = ({ artistQuery }: Props) => {
<TriangleAlert /> Important notice
</h2>
<p>
The data displayed may not be fully accurate as this artist or
band stopped performing on{" "}
Data may be inaccurate as this artist or band stopped
performing on{" "}
<strong>
{new Date(artist["life-span"].end).toLocaleDateString(
undefined,
{
year: "numeric",
month: "long",
day: "numeric",
},
)}
</strong>
Expand All @@ -106,9 +161,48 @@ const Result = ({ artistQuery }: Props) => {
</div>
)}

<SavePlaylist artistData={artistData} tracks={tracks} />
{songs && songs.length > 0 ? (
<>
<div className="bg-black bg-opacity-30 rounded-lg p-4 mb-6">
<h2 className="text-xl font-semibold mb-2">Playlist Info</h2>
<p>
Based on <strong>{data.totalTracks} songs</strong> from the
last <strong>{data.totalSetLists} concerts</strong> (
{sanitiseDate(data.from)?.toLocaleDateString(undefined, {
year: "numeric",
month: "long",
})}{" "}
to{" "}
{sanitiseDate(data.to)?.toLocaleDateString(undefined, {
year: "numeric",
month: "long",
})}
)
</p>
<p className="mt-2">
<strong>Average songs per concert</strong>:{" "}
{Math.round(data.totalTracks / data.totalSetLists)}
</p>
<p>{encoreLabel ? <p>{encoreLabel}</p> : null}</p>
<p className="mt-2">
<strong>{songs.length} songs</strong>{" "}
{playlistDuration ? (
<>
• Estimated playtime:{" "}
<strong>
{playlistDuration?.minutes} minutes and{" "}
{playlistDuration?.seconds} seconds
</strong>
</>
) : null}
</p>
</div>
<SavePlaylist artistData={artistData} songs={songs} />
</>
) : null}

<Tracks
tracks={tracks}
tracks={data.tracks}
links={artistData?.tracks}
palette={artistData?.palette}
/>
Expand Down
19 changes: 7 additions & 12 deletions components/SavePlaylist/SavePlaylist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,18 @@ import Spotify from "spotify-web-api-js";

import { useAuth } from "components/UserContext/UserContext";
import LoginBanner from "components/LoginBanner/LoginBanner";
import { matchSongs } from "utils/matchSongs";
import { Track, ArtistData } from "types";
import { ArtistData, Link } from "types";
import { CassetteTape, CircleCheckBig } from "lucide-react";

interface SavePlaylistProps {
artistData: ArtistData;
tracks: Track[];
songs: Link[];
}

const SavePlaylist = ({
artistData: { name, tracks: links },
tracks,
}: SavePlaylistProps) => {
const SavePlaylist = ({ artistData: { name }, songs }: SavePlaylistProps) => {
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
const { user } = useAuth();
if (!links || links.length === 0) {
return null;
}
const songs = matchSongs(tracks, links);
const createPlaylist = () => {
if (user) {
setLoading(true);
Expand All @@ -34,7 +26,10 @@ const SavePlaylist = ({
public: true,
})
.then((data) => {
s.addTracksToPlaylist(data.id, songs);
s.addTracksToPlaylist(
data.id,
songs.map((s) => s.uri),
);
setDone(true);
})
.finally(() => setLoading(false));
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"axios": "^1.7.7",
"classnames": "^2.5.1",
"jsonpath-plus": "^9.0.0",
"lucide-react": "^0.445.0",
"modern-css-reset": "^1.4.0",
"next": "^14.2.13",
Expand Down
1 change: 1 addition & 0 deletions pages/api/artists/[artistName]/concerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default async (req: NextApiRequest, res: NextApiResponse<Event[]>) => {
const events = await getArtistEvent(artistName, clientIp);
res.status(HttpStatusCode.Ok).json(events);
} catch (e: any) {
console.error(e);
res
.status(e?.response?.data?.code ?? HttpStatusCode.InternalServerError)
.end(e?.response?.data?.message || "Ops! There was a problem!");
Expand Down
1 change: 1 addition & 0 deletions pages/api/tracks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
const setList = await getArtistSetlist(artistName, artistId);
res.status(HttpStatusCode.Ok).json(getAggregatedSetlists(setList));
} catch (e: any) {
console.error(e);
res
.status(e?.response?.data?.code ?? HttpStatusCode.InternalServerError)
.end(e?.response?.data?.message || "Ops! There was a problem!");
Expand Down
40 changes: 40 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions server/apis/spotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ const getSongs = async (artistName: string, offset = 0) => {
limit: 50,
offset,
});

return body?.tracks?.items?.map((track) => ({
title: track.name.toLowerCase(),
uri: track.uri,
cover: track.album.images[2].url,
previewUrl: track.preview_url,
duration_ms: track.duration_ms,
}));
};

Expand Down
Loading

0 comments on commit 07dfcf9

Please sign in to comment.