exh-1.0.0: src/Web/Exhentai/API/Search.hs
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
module Web.Exhentai.API.Search
( SearchQuery (..),
SearchResult (..),
search,
searchRecur,
searchRecurResumable,
fetchSearchPage,
)
where
import Conduit
import Control.Effect
import Control.Effect.Bracket
import Control.Effect.Error
import Control.Effect.Exh
import Control.Monad
import Data.Maybe
import Data.Set (Set, toList, (\\))
import Data.String
import Data.Text (Text, unpack)
import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Client hiding (Cookie)
import Optics.Core
import Optics.TH
import Text.XML.Optics
import Web.Exhentai.API.Gallery
import Web.Exhentai.Parsing.Search
import Web.Exhentai.Utils
import Prelude hiding (last)
toBitField :: Set GalleryCategory -> Int
toBitField = sum . map ((2 ^) . fromEnum) . toList
data SearchQuery = SearchQuery
{ categories :: Maybe (Set GalleryCategory),
searchString :: {-# UNPACK #-} Text
}
deriving (Show, Eq)
queryArgCat :: Set GalleryCategory -> Int
queryArgCat s = toBitField $ allGalleryCats \\ s
data SearchResult = SearchResult
{ galleries :: [Gallery],
prevPage :: Maybe Text,
nextPage :: Maybe Text
}
deriving (Show, Eq)
parseSearchPage :: Document -> SearchResult
parseSearchPage d =
let galleries = mapMaybe parseGalleryLink $ d ^..: galleryPreviewElement % galleryLink
fld :: Traversal' Document Element
fld = body .// pagesElem
prevPage = do
first <- headOf fld d
first ^? pre linkOf
nextPage = do
last <- lastOf fld d
last ^? pre linkOf
in SearchResult {..}
-- | Fetch a search page using a 'Request'
fetchSearchPage' ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
Request ->
m SearchResult
fetchSearchPage' req = do
d <- htmlRequest req
pure $ parseSearchPage d
{-# INLINEABLE fetchSearchPage' #-}
-- | Fetch a search page using its url
fetchSearchPage ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
Text ->
m SearchResult
fetchSearchPage = fetchSearchPage' <=< formRequest . unpack
{-# INLINEABLE fetchSearchPage #-}
-- | Search a search query
search ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
SearchQuery ->
m SearchResult
search SearchQuery {..} = do
let catQ = maybe [] (\c -> [("f_cats", Just $ fromString $ show $ queryArgCat c)]) categories
initReq <- formRequest "https://exhentai.org"
let req =
setQueryString
( catQ
++ [ ("f_search", Just $ encodeUtf8 searchString)
]
)
initReq
fetchSearchPage' req
{-# INLINEABLE search #-}
-- | Iterate through all the Galleries asosciated with a search query, putting them into a stream
searchRecur ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
SearchQuery ->
ConduitT i Gallery m ()
searchRecur q = do
SearchResult {..} <- lift $ search q
yieldMany galleries
case nextPage of
Nothing -> pure ()
Just url -> searchRecur' url
{-# INLINEABLE searchRecur #-}
searchRecur' ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
-- | url
Text ->
ConduitT i Gallery m ()
searchRecur' url = do
SearchResult {..} <- lift $ fetchSearchPage url
yieldMany galleries
case nextPage of
Nothing -> pure ()
Just url' -> searchRecur' url'
{-# INLINEABLE searchRecur' #-}
-- | A resumable version of 'searchRecur' that reports it's progress.
searchRecurResumable ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
SearchQuery ->
ConduitT i (Either Text Gallery) m ()
searchRecurResumable q = do
SearchResult {..} <- lift $ search q
yieldMany $ map Right galleries
case nextPage of
Nothing -> pure ()
Just url -> searchRecurResumable' url
{-# INLINEABLE searchRecurResumable #-}
searchRecurResumable' ::
Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>
-- | url
Text ->
ConduitT i (Either Text Gallery) m ()
searchRecurResumable' url = do
yield $ Left url
SearchResult {..} <- lift $ fetchSearchPage url
yieldMany $ map Right galleries
case nextPage of
Nothing -> pure ()
Just url' -> searchRecurResumable' url'
{-# INLINEABLE searchRecurResumable' #-}
makeFieldLabelsWith noPrefixFieldLabels ''SearchQuery
makeFieldLabelsWith noPrefixFieldLabels ''SearchResult