lrclib-client 0.2.2 → 0.2.3
raw patch · 3 files changed
+118/−93 lines, 3 filesdep +http-clientdep +http-client-tlsdep +http-typesdep −lensdep −wreqdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: http-client, http-client-tls, http-types
Dependencies removed: lens, wreq
Dependency ranges changed: base
API changes (from Hackage documentation)
- LrcLib.Types: OK :: TrackData -> GetResponse
- LrcLib.Types: PublishOK :: PublishResponse
- LrcLib.Types: data GetResponse
- LrcLib.Types: data PublishResponse
- LrcLib.Types: instance Data.Aeson.Types.FromJSON.FromJSON LrcLib.Types.GetResponse
- LrcLib.Types: instance GHC.Generics.Generic LrcLib.Types.Challenge
- LrcLib.Types: instance GHC.Generics.Generic LrcLib.Types.GetResponse
- LrcLib.Types: instance GHC.Generics.Generic LrcLib.Types.PublishRequest
- LrcLib.Types: instance GHC.Generics.Generic LrcLib.Types.TrackData
- LrcLib.Types: instance GHC.Read.Read LrcLib.Types.Challenge
- LrcLib.Types: instance GHC.Read.Read LrcLib.Types.TrackData
- LrcLib.Types: instance GHC.Show.Show LrcLib.Types.Challenge
- LrcLib.Types: instance GHC.Show.Show LrcLib.Types.GetResponse
- LrcLib.Types: instance GHC.Show.Show LrcLib.Types.TrackData
- LrcLib.Types: type Url = String
+ LrcLib.Types: data GetError
+ LrcLib.Types: data PublishError
+ LrcLib.Types: instance Data.Aeson.Types.FromJSON.FromJSON LrcLib.Types.GetError
+ LrcLib.Types: instance GHC.Internal.Generics.Generic LrcLib.Types.Challenge
+ LrcLib.Types: instance GHC.Internal.Generics.Generic LrcLib.Types.GetError
+ LrcLib.Types: instance GHC.Internal.Generics.Generic LrcLib.Types.PublishRequest
+ LrcLib.Types: instance GHC.Internal.Generics.Generic LrcLib.Types.TrackData
+ LrcLib.Types: instance GHC.Internal.Read.Read LrcLib.Types.Challenge
+ LrcLib.Types: instance GHC.Internal.Read.Read LrcLib.Types.TrackData
+ LrcLib.Types: instance GHC.Internal.Show.Show LrcLib.Types.Challenge
+ LrcLib.Types: instance GHC.Internal.Show.Show LrcLib.Types.GetError
+ LrcLib.Types: instance GHC.Internal.Show.Show LrcLib.Types.TrackData
- LrcLib.Client: getCachedLyrics :: Text -> Text -> Text -> Integer -> API GetResponse
+ LrcLib.Client: getCachedLyrics :: Text -> Text -> Text -> Integer -> API (Either GetError TrackData)
- LrcLib.Client: getLyrics :: Text -> Text -> Text -> Integer -> API GetResponse
+ LrcLib.Client: getLyrics :: Text -> Text -> Text -> Integer -> API (Either GetError TrackData)
- LrcLib.Client: getLyricsById :: Integer -> API GetResponse
+ LrcLib.Client: getLyricsById :: HasCallStack => Integer -> API (Either GetError TrackData)
- LrcLib.Client: publish :: PublishRequest -> API PublishResponse
+ LrcLib.Client: publish :: HasCallStack => PublishRequest -> API (Either PublishError ())
- LrcLib.Client: publish' :: PublishToken -> PublishRequest -> API PublishResponse
+ LrcLib.Client: publish' :: HasCallStack => PublishToken -> PublishRequest -> API (Either PublishError ())
- LrcLib.Client: runAPI :: Url -> API a -> IO a
+ LrcLib.Client: runAPI :: Request -> API a -> IO a
- LrcLib.Types: IncorrectToken :: PublishResponse
+ LrcLib.Types: IncorrectToken :: PublishError
- LrcLib.Types: NotFound :: GetResponse
+ LrcLib.Types: NotFound :: GetError
- LrcLib.Types: type API = ReaderT Url IO
+ LrcLib.Types: type API = ReaderT Request IO
Files
- lrclib-client.cabal +8/−8
- src/LrcLib/Client.hs +101/−71
- src/LrcLib/Types.hs +9/−14
lrclib-client.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.0 name: lrclib-client-version: 0.2.2+version: 0.2.3 synopsis: A Haskell client for the LrcLib API (lyrics database) description: This library provides types and functions to interact with the@@ -29,22 +29,22 @@ import: warnings exposed-modules: LrcLib.Client, LrcLib.Types build-depends: aeson >= 2.2.3 && < 2.3,- base >= 4.18 && < 4.21,+ base >= 4.19 && < 4.21, base16-bytestring >= 1.0.2 && < 1.1, bytestring >= 0.12.2 && < 0.13, cryptohash-sha256 >= 0.11.102 && < 0.12,- lens >= 5.3.5 && < 5.4,+ http-client >= 0.7.19 && < 0.8,+ http-client-tls >= 0.3.6 && < 0.4,+ http-types >= 0.12.4 && < 0.13, mtl >= 2.3.1 && < 2.4, text >= 2.1.3 && < 2.2,- wreq >= 0.5.4 && < 0.6,- hs-source-dirs: src- default-language: GHC2021+ default-language: GHC2024 test-suite test- import: warnings+ import: warnings type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Main.hs build-depends: aeson, base, bytestring, tasty, tasty-hunit, lrclib-client- default-language: GHC2021+ default-language: GHC2024
src/LrcLib/Client.hs view
@@ -1,24 +1,23 @@-{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -- | Module for calling LRCLIB API (<https://lrclib.net/docs>) module LrcLib.Client ( module LrcLib.Types,+ runAPI,+ runDefaultAPI, getLyrics, getCachedLyrics, getLyricsById,- requestChallenge, searchLyrics, publish',- solveChallenge, publish,- runAPI,- runDefaultAPI,+ requestChallenge,+ solveChallenge, ) where -import Control.Lens ((&), (&~), (.=), (.~), (^.)) import Control.Monad.IO.Class (liftIO) import Control.Monad.Reader (ask, runReaderT) import Crypto.Hash.SHA256 (hash)@@ -27,13 +26,21 @@ import Data.ByteString.Char8 qualified as BC import Data.ByteString.Lazy (ByteString) import Data.Either (fromRight)-import Data.Foldable (toList) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding (encodeUtf8) import GHC.Stack import LrcLib.Types-import Network.Wreq+import Network.HTTP.Client+ ( Request (method, path, queryString, requestBody, requestHeaders),+ RequestBody (RequestBodyLBS),+ Response (responseBody, responseStatus),+ httpLbs,+ httpNoBody,+ parseRequest_,+ )+import Network.HTTP.Client.TLS (getGlobalManager)+import Network.HTTP.Types (Status (statusCode), renderSimpleQuery) import Prelude hiding (id) decode :: (A.FromJSON a, HasCallStack) => ByteString -> a@@ -42,64 +49,71 @@ Right y -> y -- | Run API action with given API url-runAPI :: Url -> API a -> IO a+runAPI :: Request -> API a -> IO a runAPI url f = runReaderT f url -- | Run API action ('runAPI') with default API url -- -- >>> runDefaultAPI $ getLyricsById 1337--- OK Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français+-- Right Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français runDefaultAPI :: API a -> IO a-runDefaultAPI = runAPI "http://lrclib.net/api"+runDefaultAPI = runAPI $ parseRequest_ "https://lrclib.net/api" -getUrl :: String -> Text -> Text -> Text -> Integer -> API GetResponse-getUrl url track artist album duration = do- apiUrl <- ask- resp <- liftIO $ getWith opts $ apiUrl <> url- case resp ^. responseStatus . statusCode of- 404 -> pure NotFound- 200 -> pure $ OK $ decode (resp ^. responseBody)+get' :: String -> Text -> Text -> Text -> Integer -> API (Either GetError TrackData)+get' subpath track artist album duration = do+ api <- ask+ let req =+ api+ { path = api.path <> BC.pack subpath,+ queryString =+ renderSimpleQuery+ False+ [ ("track_name", encodeUtf8 track),+ ("artist_name", encodeUtf8 artist),+ ("album_name", encodeUtf8 album),+ ("duration", BC.pack $ show duration)+ ]+ }+ man <- liftIO getGlobalManager+ resp <- liftIO $ httpLbs req man+ case resp.responseStatus.statusCode of+ 404 -> pure $ Left NotFound+ 200 -> pure $ Right $ decode resp.responseBody s -> error $ "Unexpected status code on get endpoint: " <> show s- where- opts =- defaults &~ do- checkResponse .= (Just $ \_ _ -> pure ())- param "track_name" .= [track]- param "artist_name" .= [artist]- param "album_name" .= [album]- param "duration" .= [T.pack $ show duration] -getLyrics, getCachedLyrics :: Text -> Text -> Text -> Integer -> API GetResponse+getLyrics,+ getCachedLyrics ::+ Text -> Text -> Text -> Integer -> API (Either GetError TrackData) -- | Get lyrics by track name, artist, album and duration -- Calls @\/api\/get@ -- -- >>> runDefaultAPI $ getLyrics "ThisTrackDoesntExist" "UnknownArtist" "UnknownAlbum" 1337--- NotFound+-- Left NotFound -- -- >>> runDefaultAPI $ getLyrics "Speak Français" "Ellis feat. NOËP" "Speak Français" 201--- OK Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français-getLyrics = getUrl "/get"+-- Right Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français+getLyrics = get' "/get" -- | The same as 'getLyrics', but for cached lyrics. -- Calls @\/api\/get-cached@-getCachedLyrics = getUrl "/get-cached"+getCachedLyrics = get' "/get-cached" -- | Get lyrics by id -- Calls @\/api\/get\/\<id\>@ -- -- >>> runDefaultAPI $ getLyricsById 1337--- OK Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français-getLyricsById :: Integer -> API GetResponse-getLyricsById id' = do- url <- ask- resp <- liftIO $ getWith opts $ url <> "/get/" <> show id'- case resp ^. responseStatus . statusCode of- 404 -> pure NotFound- 200 -> pure $ OK $ decode (resp ^. responseBody)+-- Right Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français+getLyricsById :: (HasCallStack) => Integer -> API (Either GetError TrackData)+getLyricsById id = do+ api <- ask+ let req = api {path = api.path <> "/get/" <> BC.pack (show id)}+ man <- liftIO getGlobalManager+ resp <- liftIO $ httpLbs req man+ case resp.responseStatus.statusCode of+ 404 -> pure $ Left NotFound+ 200 -> pure $ Right $ decode resp.responseBody _ -> error "Unexpected status code on get endpoint"- where- opts = defaults &~ checkResponse .= (Just $ \_ _ -> pure ()) -- | Search lyrics by either text query or track-artist-album -- Calls @\/api\/search@@@ -108,31 +122,45 @@ -- Track, id: 1337, name: Speak Français, artist: Ellis feat. NOËP, album: Speak Français searchLyrics :: SearchQuery -> API SearchResponse searchLyrics q = do- url <- ask- resp <- liftIO $ getWith opts $ url <> "/search"- pure $ decode (resp ^. responseBody)- where- opts =- defaults &~ case q of- TextQuery t -> param "q" .= [t]- TrackQuery {..} -> do- param "track_name" .= [queryName]- param "artist_name" .= toList queryArtist- param "album_name" .= toList queryAlbum+ api <- ask+ let req =+ api+ { path = api.path <> "/search",+ queryString = renderSimpleQuery False $+ case q of+ TextQuery t -> [("q", encodeUtf8 t)]+ TrackQuery {..} ->+ [("track_name", encodeUtf8 queryName)]+ ++ [ ("artist_name", encodeUtf8 artist)+ | Just artist <- pure queryArtist+ ]+ ++ [ ("album_name", encodeUtf8 album)+ | Just album <- pure queryAlbum+ ]+ }+ man <- liftIO getGlobalManager+ resp <- liftIO $ httpLbs req man+ pure $ decode resp.responseBody -- | Publish Lyrics ('publish') without requesting and solving challenge -- Calls @\/api\/publish@-publish' :: PublishToken -> PublishRequest -> API PublishResponse+publish' :: (HasCallStack) => PublishToken -> PublishRequest -> API (Either PublishError ()) publish' token request = do- url <- ask- res <- liftIO $ postWith opts (url <> "/publish") body- case res ^. responseStatus . statusCode of- 400 -> pure IncorrectToken- 201 -> pure PublishOK+ api <- ask+ let req =+ api+ { method = "POST",+ path = api.path <> "/publish",+ requestHeaders =+ api.requestHeaders <> [("X-Publish-Token", encodeUtf8 token)],+ requestBody = RequestBodyLBS $ A.encode request+ }+ man <- liftIO getGlobalManager+ res <- liftIO $ httpNoBody req man+ case res.responseStatus.statusCode of+ 400 -> pure $ Left IncorrectToken+ 201 -> pure $ Right () s -> error $ "Unexpected status code on publish endpoint: " <> show s- where- body = A.toJSON request- opts = defaults & header "X-Publish-Token" .~ [encodeUtf8 token] -- | Request proof-of-work challenge for publish -- Calls @\/api\/request-challenge@@@ -141,16 +169,11 @@ -- Challenge {prefix = "BVNC...XoVu1H", target = "000000FF0...00000"} requestChallenge :: API Challenge requestChallenge = do- url <- ask- resp <- liftIO $ post (url <> "/request-challenge") $ A.toJSON ()- pure $ decode (resp ^. responseBody)---- | Publish Lyrics (request and solve challenge)--- Calls @\/api\/publish@-publish :: PublishRequest -> API PublishResponse-publish r = do- c <- requestChallenge- publish' (solveChallenge c) r+ api <- ask+ let req = api {method = "POST", path = api.path <> "/request-challenge"}+ man <- liftIO getGlobalManager+ resp <- liftIO $ httpLbs req man+ pure $ decode resp.responseBody -- | Solve proof-of-work challenge for publish -- Solution is a one-time publish token@@ -162,3 +185,10 @@ go :: Int -> Text go n | hash (prefix' <> BC.pack (show n)) < target' = prefix <> ":" <> T.show n go n = go (n + 1)++-- | Publish Lyrics (request and solve challenge)+-- Calls @\/api\/publish@+publish :: (HasCallStack) => PublishRequest -> API (Either PublishError ())+publish r = do+ c <- requestChallenge+ publish' (solveChallenge c) r
src/LrcLib/Types.hs view
@@ -6,14 +6,13 @@ module LrcLib.Types ( API,- Url, SearchResponse, Challenge (..),- GetResponse (..),+ GetError (..), TrackData (..), SearchQuery (..),+ PublishError (..), PublishToken,- PublishResponse (..), PublishRequest (..), ) where@@ -23,12 +22,10 @@ import Data.Text (Text) import Data.Text qualified as T import GHC.Generics (Generic)---- | Representation of API url-type Url = String+import Network.HTTP.Client (Request) --- | Monad for API actions that includes API url-type API = ReaderT Url IO+-- | Monad for API actions that includes API url (as a base request)+type API = ReaderT Request IO -- | Crypto proof-of-work challenge for publish data Challenge@@ -52,10 +49,8 @@ } deriving (A.FromJSON, Generic, Eq, Read) --- | Response when getting lyrics-data GetResponse- = NotFound- | OK TrackData+-- | Error when getting lyrics+data GetError = NotFound deriving (Generic, A.FromJSON, Show) type PublishToken = Text@@ -72,8 +67,8 @@ } deriving (Generic, A.ToJSON) --- | Response when publishing lyrics-data PublishResponse = PublishOK | IncorrectToken+-- | Error when publishing lyrics+data PublishError = IncorrectToken -- | Query for search. Either text or track+artist+album data SearchQuery