packages feed

exh-0.2.0: src/Web/Exhentai/API/Search.hs

{-# LANGUAGE StrictData #-}

module Web.Exhentai.API.Search
  ( SearchQuery (..),
    SearchResult (..),
    search,
    searchRecur,
    searchRecurResumable,
    searchRecurResumable',
    fetchSearchPage,
    fetchSearchPage',
  )
where

import Conduit
import Control.Lens ((...))
import Control.Lens.Fold
import Control.Monad
import Data.Set (Set, (\\))
import Data.String
import Data.Text (Text, unpack)
import Data.Text.Encoding (encodeUtf8)
import GHC.Generics
import Network.HTTP.Client.Conduit
import Text.XML
import Web.Exhentai.Parsing.Search
import Web.Exhentai.Types
import Web.Exhentai.Types.CookieT
import Web.Exhentai.Utils
import Prelude hiding (last)

data SearchQuery = SearchQuery
  { categories :: Maybe (Set GalleryCat),
    searchString :: {-# UNPACK #-} Text
  }
  deriving (Show, Eq, Generic)

queryArgCat :: Set GalleryCat -> Int
queryArgCat s = toBitField $ allGalleryCats \\ s

data SearchResult = SearchResult
  { galleries :: [Gallery],
    prevPage :: Maybe Text,
    nextPage :: Maybe Text
  }
  deriving (Show, Eq, Generic)

parseSearchPage :: Document -> SearchResult
parseSearchPage d =
  let galleries = d ^..: galleryPreviewElement . galleryLink
      fld :: Fold Document Element
      fld = body ... pagesElem
      prevPage = do
        first <- firstOf fld d
        first ^? linkOf
      nextPage = do
        last <- lastOf fld d
        last ^? linkOf
   in SearchResult {..}

-- | Fetch a search page using a 'Request'
fetchSearchPage' :: MonadHttpState m => Request -> m SearchResult
fetchSearchPage' req = do
  d <- htmlRequest req
  pure $ parseSearchPage d

-- | Fetch a search page using its url
fetchSearchPage :: MonadHttpState m => Text -> m SearchResult
fetchSearchPage = fetchSearchPage' <=< formRequest . unpack

-- | Search a search query
search :: MonadHttpState 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

-- | Iterate through all the Galleries asosciated with a search query, putting them into a stream
searchRecur :: MonadHttpState m => SearchQuery -> ConduitT i Gallery m ()
searchRecur q = do
  SearchResult {..} <- lift $ search q
  yieldMany galleries
  case nextPage of
    Nothing -> pure ()
    Just url -> searchRecur' url

searchRecur' ::
  MonadHttpState 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'

-- | A resumable version of 'searchRecur' that reports it's progress.
searchRecurResumable :: MonadHttpState 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

searchRecurResumable' ::
  MonadHttpState 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'