I'm looking to solve a problem where I construct some data from a HTTP call and then based off that data I make another HTTP call and enrich the original data with information from the second call.
I have code which takes a Spotify Recently Played API Call (JSON) via wreq as a ByteString and returns my a fully formed "RecentlyPlayed" data type.
However, in order to get the Genre of a Track in the Spotify API, a second HTTP call is needed to their artist endpoint, I'm not quite sure how I can modify my Track data type to add a "Genre" field in that I will populate later, I'm also unsure on how to actually populate it later too, clearly I need to loop through my original data structure, pull the artist ID out, call the new server - but I'm unsure how to add this additional data to the original data type.
{-# LANGUAGE OverloadedStrings #-}
module Types.RecentlyPlayed where
import qualified Data.ByteString.Lazy as L
import qualified Data.Vector as V
import Data.Aeson
import Data.Either
data Artist = Artist {
id :: String
, href :: String
, artistName :: String
} deriving (Show)
data Track = Track {
playedAt :: String
, externalUrls :: String
, name :: String
, artists :: [Artist]
, explicit :: Bool
} deriving (Show)
data Tracks = Tracks {
tracks :: [Track]
} deriving (Show)
data RecentlyPlayed = RecentlyPlayed {
recentlyPlayed :: Tracks
, next :: String
} deriving (Show)
instance FromJSON RecentlyPlayed where
parseJSON = withObject "items" $ \recentlyPlayed -> RecentlyPlayed
<$> recentlyPlayed .: "items"
<*> recentlyPlayed .: "next"
instance FromJSON Tracks where
parseJSON = withArray "items" $ \items -> Tracks
<$> mapM parseJSON (V.toList items)
instance FromJSON Track where
parseJSON = withObject "tracks" $ \tracks -> Track
<$> tracks .: "played_at"
<*> (tracks .: "track" >>= (.: "album") >>= (.: "external_urls") >>= (.: "spotify"))
<*> (tracks .: "track" >>= (.: "name"))
<*> (tracks .: "track" >>= (.: "artists"))
<*> (tracks .: "track" >>= (.: "explicit"))
instance FromJSON Artist where
parseJSON = withObject "artists" $ \artists -> Artist
<$> artists .: "id"
<*> artists .: "href"
<*> artists .: "name"
marshallRecentlyPlayedData :: L.ByteString -> Either String RecentlyPlayed
marshallRecentlyPlayedData recentlyPlayedTracks = eitherDecode recentlyPlayedTracks
(https://github.com/imjacobclark/Recify/blob/master/src/Types/RecentlyPlayed.hs)
This works brilliantly for a single API call, its usage can be seen here:
recentlyPlayedTrackData <- liftIO $ (getCurrentUsersRecentlyPlayedTracks (textToByteString . getAccessToken . AccessToken $ accessTokenFileData))
let maybeMarshalledRecentlyPlayed = (marshallRecentlyPlayedData recentlyPlayedTrackData)
(https://github.com/imjacobclark/Recify/blob/master/src/Recify.hs#L53-L55)
{-# LANGUAGE OverloadedStrings #-}
module Clients.Spotify.RecentlyPlayed where
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as B
import qualified Network.Wreq as W
import System.Environment
import Control.Monad.IO.Class
import Control.Lens
recentlyPlayerUri = "https://api.spotify.com/v1/me/player/recently-played"
getCurrentUsersRecentlyPlayedTracks :: B.ByteString -> IO L.ByteString
getCurrentUsersRecentlyPlayedTracks accessToken = do
let options = W.defaults & W.header "Authorization" .~ [(B.pack "Bearer ") <> accessToken]
text <- liftIO $ (W.getWith options recentlyPlayerUri)
return $ text ^. W.responseBody
(https://github.com/imjacobclark/Recify/blob/master/src/Clients/Spotify/RecentlyPlayed.hs)
I expect to be able to call the first API, construct my data type, call the second API and then enrich the first data type with data returned from the second HTTP call.