exh-1.0.0: src/Web/Exhentai/API/MPV.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
-- | MPV (multi-page viewer) API.
module Web.Exhentai.API.MPV
( DispatchRequest (..),
Server (..),
Dim (..),
buildRequest,
fetchImage,
)
where
import Conduit
import Control.Applicative
import Control.Effect
import Control.Effect.Bracket
import Control.Effect.Error
import Control.Effect.Exh
import Control.Monad
import Control.Monad.Trans.Cont
import Data.Aeson
import Data.ByteString (ByteString)
import Data.Text (Text, pack, unpack)
import Language.JavaScript.Extraction
import Language.JavaScript.Parser
import Network.HTTP.Client hiding (Cookie)
import Optics.Core
import Optics.TH
import Text.XML
import Text.XML.Optics
import Web.Exhentai.API.Gallery
import Web.Exhentai.Errors
import Web.Exhentai.Utils
import Prelude hiding ((!!))
data Server
= HAtH {-# UNPACK #-} Int
| Other {-# UNPACK #-} Text
deriving (Show, Eq)
instance FromJSON Server where
parseJSON v =
HAtH <$> parseJSON v <|> Other <$> parseJSON v
instance ToJSON Server where
toJSON (HAtH i) = toJSON i
toJSON (Other t) = toJSON t
newtype Dim = Dim Int
deriving newtype (Show, Eq)
instance FromJSON Dim where
parseJSON v = Dim <$> str <|> Dim <$> int
where
str = read <$> parseJSON v
int = parseJSON v
data DispatchResult = DispatchResult
{ -- | A piece of text describing the dimensions and the size of this image
dimension :: {-# UNPACK #-} Text,
-- | The path part of the url pointing to the original image
origImgPath :: {-# UNPACK #-} Text,
-- | The path part of the url that searches for the gallery containing this image
searchPath :: {-# UNPACK #-} Text,
-- | The path part of the non-mpv page that displays this image
galleryPath :: {-# UNPACK #-} Text,
width :: {-# UNPACK #-} Dim,
height :: {-# UNPACK #-} Dim,
-- | The full url to this image
imgLink :: {-# UNPACK #-} Text,
-- | The server that serves this image
server :: Server
}
deriving (Show, Eq)
instance FromJSON DispatchResult where
parseJSON = withObject "imagedispatch result" $ \o ->
DispatchResult
<$> o .: "d"
<*> o .: "lf"
<*> o .: "ls"
<*> o .: "lo"
<*> o .: "xres"
<*> o .: "yres"
<*> o .: "i"
<*> o .: "s"
data DispatchRequest = DispatchRequest
{ galleryId :: {-# UNPACK #-} Int,
page :: {-# UNPACK #-} Int,
imgKey :: {-# UNPACK #-} Text,
mpvKey :: {-# UNPACK #-} Text,
exclude :: Maybe Server
}
deriving (Show, Eq)
instance ToJSON DispatchRequest where
toJSON DispatchRequest {..} = object l
where
exc = maybe [] (\s -> ["nl" .= s]) exclude
l =
exc
++ [ "method" .= ("imagedispatch" :: Text),
"gid" .= galleryId,
"page" .= page,
"imgkey" .= imgKey,
"mpvkey" .= mpvKey
]
-- | Generate a list of requests from a 'Vars'
toRequests :: Vars -> [DispatchRequest]
toRequests Vars {..} = zipWith formReq [1 ..] imageList
where
formReq i MpvImage {..} =
DispatchRequest
{ galleryId = gid,
page = i,
imgKey = key,
exclude = Nothing,
mpvKey = mpvkey
}
toMpvLink :: Gallery -> Text
toMpvLink Gallery {..} = "https://exhentai.org/mpv/" <> pack (show galleryId) <> "/" <> token <> "/"
-- | Fetch the 'Vars' from a Gallery's mpv page
fetchMpv ::
Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>
Gallery ->
m Vars
fetchMpv g = htmlRequest' (toMpvLink g) >>= parseMpv
{-# INLINEABLE fetchMpv #-}
parseMpv :: Effs '[Throw ExhentaiError] m => Document -> m Vars
parseMpv doc = do
let script = foldOf allScripts doc
mast = parse (unpack script) ""
case mast of
Left _ -> error "impossible, javascript parse failed"
Right ast ->
case as ast of
Nothing -> throw ExtractionFailure
Just vars -> pure vars
{-# INLINEABLE parseMpv #-}
-- | Build dispatch requests for a gallery
buildRequest ::
Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>
Gallery ->
m [DispatchRequest]
buildRequest g = toRequests <$> fetchMpv g
{-# INLINEABLE buildRequest #-}
-- | Calls the API to dispatch a image request to a H@H server
imageDispatch ::
Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>
DispatchRequest ->
m DispatchResult
imageDispatch dreq = do
initReq <- formRequest "https://exhentai.org/api.php"
let req = initReq {method = "POST", requestBody = RequestBodyLBS $ encode dreq}
r <- jsonRequest req
case r of
Left e -> throw $ JSONParseFailure e
Right res -> pure res
{-# INLINEABLE imageDispatch #-}
-- | Fetch an image with a 'DispatchRequest'
fetchImage ::
Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>
DispatchRequest ->
ContT r m (Response (ConduitT i ByteString IO ()))
fetchImage dreq = ContT $ \k -> bracket (fetchImage' dreq) respClose k
{-# INLINEABLE fetchImage #-}
-- | Like 'fetchImage', but the user is responsible of closing the response
fetchImage' ::
Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>
DispatchRequest ->
m (Response (ConduitT i ByteString IO ()))
fetchImage' dreq = do
res <- imageDispatch dreq
req <- formRequest $ unpack $ imgLink res
openWithJar req `catch` \(_ :: HttpException) -> do
res' <- imageDispatch $ dreq {exclude = Just $ server res}
req' <- formRequest $ unpack $ imgLink res'
openWithJar req' `catch` \(_ :: HttpException) -> do
req'' <- formRequest $ unpack $ "https://exhentai.org/" <> origImgPath res
openWithJar req''
{-# INLINEABLE fetchImage' #-}
allScripts :: Traversal' Document Text
allScripts = body .// (scripts % lower %> _Content)
makeFieldLabelsWith noPrefixFieldLabels ''DispatchResult
makeFieldLabelsWith noPrefixFieldLabels ''DispatchRequest
makePrismLabels ''Dim
makePrismLabels ''Server