exh 0.2.0 → 1.0.0
raw patch · 20 files changed
+970/−739 lines, 20 filesdep +in-other-wordsdep +language-javascriptdep +optics-coredep −exceptionsdep −http-conduitdep −lensdep ~http-client-tlsdep ~transformersPVP ok
version bump matches the API change (PVP)
Dependencies added: in-other-words, language-javascript, optics-core, optics-th, xml-optics
Dependencies removed: exceptions, http-conduit, lens, monad-control, monad-time, mtl, quickjs-hs, retry, transformers-base, xml-lens
Dependency ranges changed: http-client-tls, transformers
API changes (from Hackage documentation)
- Web.Exhentai.API.Auth: instance GHC.Generics.Generic Web.Exhentai.API.Auth.Credential
- Web.Exhentai.API.Gallery: instance GHC.Generics.Generic Web.Exhentai.API.Gallery.GalleryInfo
- Web.Exhentai.API.Gallery: instance GHC.Generics.Generic Web.Exhentai.API.Gallery.Visibility
- Web.Exhentai.API.MPV: DispatchResult :: Text -> Text -> Text -> Text -> Dim -> Dim -> Text -> Server -> DispatchResult
- Web.Exhentai.API.MPV: Vars :: {-# UNPACK #-}Int -> {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> {-# UNPACK #-}Int -> [MpvImage] -> Vars
- Web.Exhentai.API.MPV: [$sel:apiUrl:Vars] :: Vars -> {-# UNPACK #-}Text
- Web.Exhentai.API.MPV: [$sel:dimension:DispatchResult] :: DispatchResult -> Text
- Web.Exhentai.API.MPV: [$sel:galleryPath:DispatchResult] :: DispatchResult -> Text
- Web.Exhentai.API.MPV: [$sel:gid:Vars] :: Vars -> {-# UNPACK #-}Int
- Web.Exhentai.API.MPV: [$sel:height:DispatchResult] :: DispatchResult -> Dim
- Web.Exhentai.API.MPV: [$sel:imageList:Vars] :: Vars -> [MpvImage]
- Web.Exhentai.API.MPV: [$sel:imgLink:DispatchResult] :: DispatchResult -> Text
- Web.Exhentai.API.MPV: [$sel:mpvkey:Vars] :: Vars -> {-# UNPACK #-}Text
- Web.Exhentai.API.MPV: [$sel:origImgPath:DispatchResult] :: DispatchResult -> Text
- Web.Exhentai.API.MPV: [$sel:pageCount:Vars] :: Vars -> {-# UNPACK #-}Int
- Web.Exhentai.API.MPV: [$sel:searchPath:DispatchResult] :: DispatchResult -> Text
- Web.Exhentai.API.MPV: [$sel:server:DispatchResult] :: DispatchResult -> Server
- Web.Exhentai.API.MPV: [$sel:width:DispatchResult] :: DispatchResult -> Dim
- Web.Exhentai.API.MPV: data DispatchResult
- Web.Exhentai.API.MPV: data Vars
- Web.Exhentai.API.MPV: fetchImage' :: (MonadHttpState m, MonadIO n) => DispatchRequest -> m (Response (ConduitT i ByteString n ()))
- Web.Exhentai.API.MPV: fetchMpv :: (MonadHttpState m, MonadIO m) => Gallery -> m Vars
- Web.Exhentai.API.MPV: imageDispatch :: MonadHttpState m => DispatchRequest -> m DispatchResult
- Web.Exhentai.API.MPV: instance GHC.Generics.Generic Web.Exhentai.API.MPV.DispatchRequest
- Web.Exhentai.API.MPV: instance GHC.Generics.Generic Web.Exhentai.API.MPV.DispatchResult
- Web.Exhentai.API.MPV: instance GHC.Generics.Generic Web.Exhentai.API.MPV.Server
- Web.Exhentai.API.MPV: toRequests :: Vars -> [DispatchRequest]
- Web.Exhentai.API.Search: fetchSearchPage' :: MonadHttpState m => Request -> m SearchResult
- Web.Exhentai.API.Search: instance GHC.Generics.Generic Web.Exhentai.API.Search.SearchQuery
- Web.Exhentai.API.Search: instance GHC.Generics.Generic Web.Exhentai.API.Search.SearchResult
- Web.Exhentai.API.Search: searchRecurResumable' :: MonadHttpState m => Text -> ConduitT i (Either Text Gallery) m ()
- Web.Exhentai.Errors: ExtractionFailure :: String -> ExhentaiError
- Web.Exhentai.Errors: JSONParseFailure :: String -> ExhentaiError
- Web.Exhentai.Errors: XMLParseFailure :: Text -> Text -> ExhentaiError
- Web.Exhentai.Errors: [$sel:reason:JSONParseFailure] :: ExhentaiError -> Text
- Web.Exhentai.Errors: [$sel:url:JSONParseFailure] :: ExhentaiError -> Text
- Web.Exhentai.Errors: data ExhentaiError
- Web.Exhentai.Errors: instance GHC.Classes.Eq Web.Exhentai.Errors.ExhentaiError
- Web.Exhentai.Errors: instance GHC.Exception.Type.Exception Web.Exhentai.Errors.ExhentaiError
- Web.Exhentai.Errors: instance GHC.Generics.Generic Web.Exhentai.Errors.ExhentaiError
- Web.Exhentai.Errors: instance GHC.Show.Show Web.Exhentai.Errors.ExhentaiError
- Web.Exhentai.Parsing.Gallery: Artist :: TagCategory
- Web.Exhentai.Parsing.Gallery: Character :: TagCategory
- Web.Exhentai.Parsing.Gallery: Female :: TagCategory
- Web.Exhentai.Parsing.Gallery: Group :: TagCategory
- Web.Exhentai.Parsing.Gallery: Language :: TagCategory
- Web.Exhentai.Parsing.Gallery: Male :: TagCategory
- Web.Exhentai.Parsing.Gallery: Misc :: TagCategory
- Web.Exhentai.Parsing.Gallery: Parody :: TagCategory
- Web.Exhentai.Parsing.Gallery: Reclass :: TagCategory
- Web.Exhentai.Parsing.Gallery: _TagCategory :: Prism' Text TagCategory
- Web.Exhentai.Parsing.Gallery: data TagCategory
- Web.Exhentai.Parsing.Gallery: instance GHC.Classes.Eq Web.Exhentai.Parsing.Gallery.TagCategory
- Web.Exhentai.Parsing.Gallery: instance GHC.Enum.Enum Web.Exhentai.Parsing.Gallery.TagCategory
- Web.Exhentai.Parsing.Gallery: instance GHC.Generics.Generic Web.Exhentai.Parsing.Gallery.TagCategory
- Web.Exhentai.Parsing.Gallery: instance GHC.Show.Show Web.Exhentai.Parsing.Gallery.TagCategory
- Web.Exhentai.Parsing.Gallery: readTagCat :: Text -> Maybe TagCategory
- Web.Exhentai.Parsing.Image: nextPage :: Traversal' Element Text
- Web.Exhentai.Parsing.MPV: MpvImage :: {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> MpvImage
- Web.Exhentai.Parsing.MPV: Vars :: {-# UNPACK #-}Int -> {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> {-# UNPACK #-}Int -> [MpvImage] -> Vars
- Web.Exhentai.Parsing.MPV: [$sel:apiUrl:Vars] :: Vars -> {-# UNPACK #-}Text
- Web.Exhentai.Parsing.MPV: [$sel:gid:Vars] :: Vars -> {-# UNPACK #-}Int
- Web.Exhentai.Parsing.MPV: [$sel:imageList:Vars] :: Vars -> [MpvImage]
- Web.Exhentai.Parsing.MPV: [$sel:key:MpvImage] :: MpvImage -> {-# UNPACK #-}Text
- Web.Exhentai.Parsing.MPV: [$sel:mpvkey:Vars] :: Vars -> {-# UNPACK #-}Text
- Web.Exhentai.Parsing.MPV: [$sel:name:MpvImage] :: MpvImage -> {-# UNPACK #-}Text
- Web.Exhentai.Parsing.MPV: [$sel:pageCount:Vars] :: Vars -> {-# UNPACK #-}Int
- Web.Exhentai.Parsing.MPV: [$sel:thumbnail:MpvImage] :: MpvImage -> {-# UNPACK #-}Text
- Web.Exhentai.Parsing.MPV: allScripts :: Traversal' Document Text
- Web.Exhentai.Parsing.MPV: data MpvImage
- Web.Exhentai.Parsing.MPV: data Vars
- Web.Exhentai.Parsing.MPV: extractEnv :: Text -> IO (Result Vars)
- Web.Exhentai.Parsing.MPV: instance Data.Aeson.Types.FromJSON.FromJSON Web.Exhentai.Parsing.MPV.MpvImage
- Web.Exhentai.Parsing.MPV: instance GHC.Classes.Eq Web.Exhentai.Parsing.MPV.MpvImage
- Web.Exhentai.Parsing.MPV: instance GHC.Classes.Eq Web.Exhentai.Parsing.MPV.Vars
- Web.Exhentai.Parsing.MPV: instance GHC.Generics.Generic Web.Exhentai.Parsing.MPV.MpvImage
- Web.Exhentai.Parsing.MPV: instance GHC.Generics.Generic Web.Exhentai.Parsing.MPV.Vars
- Web.Exhentai.Parsing.MPV: instance GHC.Show.Show Web.Exhentai.Parsing.MPV.MpvImage
- Web.Exhentai.Parsing.MPV: instance GHC.Show.Show Web.Exhentai.Parsing.MPV.Vars
- Web.Exhentai.Parsing.Search: galleryLength :: Traversal' Element GalleryLength
- Web.Exhentai.Types: ArtistCG :: GalleryCat
- Web.Exhentai.Types: AsianPorn :: GalleryCat
- Web.Exhentai.Types: AverageRating :: Float -> AverageRating
- Web.Exhentai.Types: Cosplay :: GalleryCat
- Web.Exhentai.Types: Doujinshi :: GalleryCat
- Web.Exhentai.Types: FavoriteCount :: Int -> FavoriteCount
- Web.Exhentai.Types: Gallery :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !Text -> Gallery
- Web.Exhentai.Types: GalleryLength :: Int -> GalleryLength
- Web.Exhentai.Types: GameCG :: GalleryCat
- Web.Exhentai.Types: ImageSet :: GalleryCat
- Web.Exhentai.Types: Manga :: GalleryCat
- Web.Exhentai.Types: Misc :: GalleryCat
- Web.Exhentai.Types: NonH :: GalleryCat
- Web.Exhentai.Types: PopUpLink :: Text -> PopUpLink
- Web.Exhentai.Types: Private :: GalleryCat
- Web.Exhentai.Types: Western :: GalleryCat
- Web.Exhentai.Types: [$sel:galleryId:Gallery] :: Gallery -> {-# UNPACK #-} !Int
- Web.Exhentai.Types: [$sel:token:Gallery] :: Gallery -> {-# UNPACK #-} !Text
- Web.Exhentai.Types: [$sel:unFavoriteCount:FavoriteCount] :: FavoriteCount -> Int
- Web.Exhentai.Types: [$sel:unGalleryLength:GalleryLength] :: GalleryLength -> Int
- Web.Exhentai.Types: [$sel:unLink:PopUpLink] :: PopUpLink -> Text
- Web.Exhentai.Types: [$sel:unRating:AverageRating] :: AverageRating -> Float
- Web.Exhentai.Types: _AverageRating :: Prism' Text AverageRating
- Web.Exhentai.Types: _GalleryCat :: Prism' Text GalleryCat
- Web.Exhentai.Types: _GalleryLength :: Prism' Text GalleryLength
- Web.Exhentai.Types: _GalleryLink :: Prism' Text Gallery
- Web.Exhentai.Types: _PopUpLink :: Prism' Text PopUpLink
- Web.Exhentai.Types: allGalleryCats :: Set GalleryCat
- Web.Exhentai.Types: data Gallery
- Web.Exhentai.Types: data GalleryCat
- Web.Exhentai.Types: instance GHC.Classes.Eq Web.Exhentai.Types.AverageRating
- Web.Exhentai.Types: instance GHC.Classes.Eq Web.Exhentai.Types.FavoriteCount
- Web.Exhentai.Types: instance GHC.Classes.Eq Web.Exhentai.Types.Gallery
- Web.Exhentai.Types: instance GHC.Classes.Eq Web.Exhentai.Types.GalleryCat
- Web.Exhentai.Types: instance GHC.Classes.Eq Web.Exhentai.Types.GalleryLength
- Web.Exhentai.Types: instance GHC.Classes.Eq Web.Exhentai.Types.PopUpLink
- Web.Exhentai.Types: instance GHC.Classes.Ord Web.Exhentai.Types.GalleryCat
- Web.Exhentai.Types: instance GHC.Enum.Bounded Web.Exhentai.Types.GalleryCat
- Web.Exhentai.Types: instance GHC.Enum.Enum Web.Exhentai.Types.GalleryCat
- Web.Exhentai.Types: instance GHC.Generics.Generic Web.Exhentai.Types.Gallery
- Web.Exhentai.Types: instance GHC.Show.Show Web.Exhentai.Types.AverageRating
- Web.Exhentai.Types: instance GHC.Show.Show Web.Exhentai.Types.FavoriteCount
- Web.Exhentai.Types: instance GHC.Show.Show Web.Exhentai.Types.Gallery
- Web.Exhentai.Types: instance GHC.Show.Show Web.Exhentai.Types.GalleryCat
- Web.Exhentai.Types: instance GHC.Show.Show Web.Exhentai.Types.GalleryLength
- Web.Exhentai.Types: instance GHC.Show.Show Web.Exhentai.Types.PopUpLink
- Web.Exhentai.Types: newtype AverageRating
- Web.Exhentai.Types: newtype FavoriteCount
- Web.Exhentai.Types: newtype GalleryLength
- Web.Exhentai.Types: newtype PopUpLink
- Web.Exhentai.Types: parseAverageRating :: Text -> Maybe AverageRating
- Web.Exhentai.Types: parseFavoriteCount :: Text -> Maybe FavoriteCount
- Web.Exhentai.Types: parseGalleryLength :: Text -> Maybe GalleryLength
- Web.Exhentai.Types: parseGalleryLink :: Text -> Maybe Gallery
- Web.Exhentai.Types: parsePopUpLink :: Text -> Maybe PopUpLink
- Web.Exhentai.Types: parsePreviewLink :: Text -> Maybe Text
- Web.Exhentai.Types: readCat :: Text -> Maybe GalleryCat
- Web.Exhentai.Types: showCat :: GalleryCat -> Text
- Web.Exhentai.Types: toBitField :: Set GalleryCat -> Int
- Web.Exhentai.Types: toGalleryLink :: Gallery -> Text
- Web.Exhentai.Types: toMpvLink :: Gallery -> Text
- Web.Exhentai.Types: type Parser = Parsec Void Text
- Web.Exhentai.Types.CookieT: CookieEnv :: Policy -> MVar CookieJar -> {-# UNPACK #-} !Manager -> CookieEnv
- Web.Exhentai.Types.CookieT: CookieT :: ReaderT CookieEnv m a -> CookieT m a
- Web.Exhentai.Types.CookieT: Policy :: RetryPolicy -> Policy
- Web.Exhentai.Types.CookieT: [$sel:jarRef:CookieEnv] :: CookieEnv -> MVar CookieJar
- Web.Exhentai.Types.CookieT: [$sel:manager:CookieEnv] :: CookieEnv -> {-# UNPACK #-} !Manager
- Web.Exhentai.Types.CookieT: [$sel:policy:CookieEnv] :: CookieEnv -> Policy
- Web.Exhentai.Types.CookieT: [$sel:unCookieT:CookieT] :: CookieT m a -> ReaderT CookieEnv m a
- Web.Exhentai.Types.CookieT: attachFormData :: MonadHttp m => [Part] -> Request -> m Request
- Web.Exhentai.Types.CookieT: class (Monad m, MonadCatch m, MonadThrow m) => MonadHttp m
- Web.Exhentai.Types.CookieT: class (MonadMask m, MonadTime m, MonadHttp m, MonadIO m) => MonadHttpState m
- Web.Exhentai.Types.CookieT: data CookieEnv
- Web.Exhentai.Types.CookieT: formRequest :: MonadHttp m => String -> m Request
- Web.Exhentai.Types.CookieT: getRetryPolicy :: MonadHttp m => m Policy
- Web.Exhentai.Types.CookieT: instance (Control.Monad.Catch.MonadMask m, Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Time.MonadTime m) => Web.Exhentai.Types.CookieT.MonadHttpState (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance (Control.Monad.IO.Class.MonadIO m, Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Catch.MonadCatch m, Control.Monad.Catch.MonadThrow m) => Web.Exhentai.Types.CookieT.MonadHttp (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance (Web.Exhentai.Types.CookieT.MonadHttp m, Control.Monad.Trans.Class.MonadTrans f, GHC.Base.Monad (f m), Control.Monad.Catch.MonadCatch (f m), Control.Monad.Catch.MonadThrow (f m)) => Web.Exhentai.Types.CookieT.MonadHttp (f m)
- Web.Exhentai.Types.CookieT: instance (Web.Exhentai.Types.CookieT.MonadHttpState m, Control.Monad.Trans.Class.MonadTrans f, Control.Monad.Catch.MonadMask (f m), Control.Monad.Time.MonadTime (f m), Web.Exhentai.Types.CookieT.MonadHttp (f m), Control.Monad.IO.Class.MonadIO (f m)) => Web.Exhentai.Types.CookieT.MonadHttpState (f m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Base.MonadBase b m => Control.Monad.Base.MonadBase b (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Catch.MonadMask m => Control.Monad.Catch.MonadMask (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Error.Class.MonadError e m => Control.Monad.Error.Class.MonadError e (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Control.Monad.IO.Unlift.MonadUnliftIO (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Time.MonadTime m => Control.Monad.Time.MonadTime (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Trans.Class.MonadTrans Web.Exhentai.Types.CookieT.CookieT
- Web.Exhentai.Types.CookieT: instance Control.Monad.Trans.Control.MonadBaseControl b m => Control.Monad.Trans.Control.MonadBaseControl b (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance Control.Monad.Trans.Resource.Internal.MonadResource m => Control.Monad.Trans.Resource.Internal.MonadResource (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance GHC.Base.Applicative m => GHC.Base.Applicative (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance GHC.Base.Functor m => GHC.Base.Functor (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader Web.Exhentai.Types.CookieT.CookieEnv (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance GHC.Base.Monad m => GHC.Base.Monad (Web.Exhentai.Types.CookieT.CookieT m)
- Web.Exhentai.Types.CookieT: instance GHC.Generics.Generic Web.Exhentai.Types.CookieT.CookieEnv
- Web.Exhentai.Types.CookieT: instance Network.HTTP.Client.Types.HasHttpManager Web.Exhentai.Types.CookieT.CookieEnv
- Web.Exhentai.Types.CookieT: modifyingJar :: MonadHttpState m => Request -> m ()
- Web.Exhentai.Types.CookieT: newtype CookieT m a
- Web.Exhentai.Types.CookieT: newtype Policy
- Web.Exhentai.Types.CookieT: openWithJar :: (MonadHttpState m, MonadIO n) => Request -> m (Response (ConduitT i ByteString n ()))
- Web.Exhentai.Types.CookieT: putCookieJar :: MonadHttpState m => CookieJar -> m ()
- Web.Exhentai.Types.CookieT: readCookieJar :: MonadHttpState m => m CookieJar
- Web.Exhentai.Types.CookieT: reqNoBody :: MonadHttp m => Request -> m (Response ())
- Web.Exhentai.Types.CookieT: respClose :: MonadHttp m => Response body -> m ()
- Web.Exhentai.Types.CookieT: respOpen :: (MonadHttp m, MonadIO n) => Request -> m (Response (ConduitT i ByteString n ()))
- Web.Exhentai.Types.CookieT: retryWhenTimeout :: MonadHttpState m => m a -> m a
- Web.Exhentai.Types.CookieT: runCookieT :: MonadIO m => RetryPolicy -> CookieT m a -> m a
- Web.Exhentai.Types.CookieT: takeCookieJar :: MonadHttpState m => m CookieJar
- Web.Exhentai.Types.CookieT: withJar :: (MonadHttpState m, MonadIO n) => Request -> (ConduitT i ByteString n () -> m a) -> m a
- Web.Exhentai.Utils: attributeSatisfies' :: Name -> (Maybe Text -> Bool) -> Traversal' Element Element
- Web.Exhentai.Utils: lower :: Traversal' Element Node
- Web.Exhentai.Utils: withoutAttribute :: Name -> Traversal' Element Element
+ Web.Exhentai: ExtractionFailure :: ExhentaiError
+ Web.Exhentai: JSONParseFailure :: String -> ExhentaiError
+ Web.Exhentai: XMLParseFailure :: Text -> Text -> ExhentaiError
+ Web.Exhentai: [$sel:reason:JSONParseFailure] :: ExhentaiError -> Text
+ Web.Exhentai: [$sel:url:JSONParseFailure] :: ExhentaiError -> Text
+ Web.Exhentai: [PutCookie] :: CookieJar -> Cookie m ()
+ Web.Exhentai: [ReadCookie] :: Cookie m CookieJar
+ Web.Exhentai: [RunConduitIO] :: ConduitT () Void IO a -> ConduitIO m a
+ Web.Exhentai: [TakeCookie] :: Cookie m CookieJar
+ Web.Exhentai: conduitIOToIO :: Eff (Embed IO) m => ConduitIOToIOC m a -> m a
+ Web.Exhentai: cookieToIO :: (Eff (Embed IO) m, Threaders '[ReaderThreads] m p) => CookieToIOC m a -> m a
+ Web.Exhentai: data ConduitIO :: Effect
+ Web.Exhentai: data ConduitIOH
+ Web.Exhentai: data Cookie :: Effect
+ Web.Exhentai: data CookieH
+ Web.Exhentai: data ExhentaiError
+ Web.Exhentai: data Http :: Effect
+ Web.Exhentai: data HttpH
+ Web.Exhentai: exhToIO :: (Effs '[Embed IO, Reader Manager] m, Threaders '[ReaderThreads] m p) => ExhC m a -> m a
+ Web.Exhentai: formRequest :: Effs '[Http, Error HttpException] m => String -> m Request
+ Web.Exhentai: genBoundary :: Eff Http m => m ByteString
+ Web.Exhentai: httpToIO :: (Effs '[Embed IO, Reader Manager] m, Threaders '[ReaderThreads] m p) => HttpToIOC m a -> m a
+ Web.Exhentai: putCookie :: Eff Cookie m => CookieJar -> m ()
+ Web.Exhentai: readCookie :: Eff Cookie m => m CookieJar
+ Web.Exhentai: respClose :: Eff Http m => Response a -> m ()
+ Web.Exhentai: respOpen :: Effs '[Http, Error HttpException] m => Request -> m (Response BodyReader)
+ Web.Exhentai: runConduitIO :: Eff ConduitIO m => ConduitT () Void IO a -> m a
+ Web.Exhentai: takeCookie :: Eff Cookie m => m CookieJar
+ Web.Exhentai: type ConduitIOToIOC = InterpretC ConduitIOH ConduitIO
+ Web.Exhentai: type CookieToIOC = CompositionC '[CookieToReaderC, ReaderC (MVar CookieJar)]
+ Web.Exhentai: type ExhC = CompositionC '[HttpToIOC, ConduitIOToIOC, CookieToIOC]
+ Web.Exhentai: type HttpToIOC = InterpretC HttpH Http
+ Web.Exhentai.API.Auth: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.ByteString.Internal.ByteString, b GHC.Types.~ Data.ByteString.Internal.ByteString) => Optics.Internal.Optic.LabelOptic "password" k Web.Exhentai.API.Auth.Credential Web.Exhentai.API.Auth.Credential a b
+ Web.Exhentai.API.Auth: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.ByteString.Internal.ByteString, b GHC.Types.~ Data.ByteString.Internal.ByteString) => Optics.Internal.Optic.LabelOptic "username" k Web.Exhentai.API.Auth.Credential Web.Exhentai.API.Auth.Credential a b
+ Web.Exhentai.API.Gallery: Artist :: TagCategory
+ Web.Exhentai.API.Gallery: ArtistCG :: GalleryCategory
+ Web.Exhentai.API.Gallery: AsianPorn :: GalleryCategory
+ Web.Exhentai.API.Gallery: Character :: TagCategory
+ Web.Exhentai.API.Gallery: Cosplay :: GalleryCategory
+ Web.Exhentai.API.Gallery: Doujinshi :: GalleryCategory
+ Web.Exhentai.API.Gallery: Female :: TagCategory
+ Web.Exhentai.API.Gallery: Gallery :: {-# UNPACK #-} !Int -> {-# UNPACK #-} !Text -> Gallery
+ Web.Exhentai.API.Gallery: GameCG :: GalleryCategory
+ Web.Exhentai.API.Gallery: Group :: TagCategory
+ Web.Exhentai.API.Gallery: ImageSet :: GalleryCategory
+ Web.Exhentai.API.Gallery: Language :: TagCategory
+ Web.Exhentai.API.Gallery: Male :: TagCategory
+ Web.Exhentai.API.Gallery: Manga :: GalleryCategory
+ Web.Exhentai.API.Gallery: Misc :: GalleryCategory
+ Web.Exhentai.API.Gallery: Misc' :: TagCategory
+ Web.Exhentai.API.Gallery: NonH :: GalleryCategory
+ Web.Exhentai.API.Gallery: Parody :: TagCategory
+ Web.Exhentai.API.Gallery: Private :: GalleryCategory
+ Web.Exhentai.API.Gallery: Reclass :: TagCategory
+ Web.Exhentai.API.Gallery: Western :: GalleryCategory
+ Web.Exhentai.API.Gallery: [$sel:galleryId:Gallery] :: Gallery -> {-# UNPACK #-} !Int
+ Web.Exhentai.API.Gallery: [$sel:token:Gallery] :: Gallery -> {-# UNPACK #-} !Text
+ Web.Exhentai.API.Gallery: allGalleryCats :: Set GalleryCategory
+ Web.Exhentai.API.Gallery: data Gallery
+ Web.Exhentai.API.Gallery: data GalleryCategory
+ Web.Exhentai.API.Gallery: data TagCategory
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "archiverLink" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "language" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "previewLink" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "title" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "torrentLink" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "uploader" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Time.Clock.Internal.UTCTime.UTCTime, b GHC.Types.~ Data.Time.Clock.Internal.UTCTime.UTCTime) => Optics.Internal.Optic.LabelOptic "uploadTime" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe Data.Text.Internal.Text, b GHC.Types.~ GHC.Maybe.Maybe Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "jaTitle" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe Web.Exhentai.API.Gallery.Gallery, b GHC.Types.~ GHC.Maybe.Maybe Web.Exhentai.API.Gallery.Gallery) => Optics.Internal.Optic.LabelOptic "newer" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe Web.Exhentai.API.Gallery.Gallery, b GHC.Types.~ GHC.Maybe.Maybe Web.Exhentai.API.Gallery.Gallery) => Optics.Internal.Optic.LabelOptic "parent" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Types.Double, b GHC.Types.~ GHC.Types.Double) => Optics.Internal.Optic.LabelOptic "rating" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "favoriteCount" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "length" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "ratingCount" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Web.Exhentai.API.Gallery.GalleryCategory, b GHC.Types.~ Web.Exhentai.API.Gallery.GalleryCategory) => Optics.Internal.Optic.LabelOptic "category" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Web.Exhentai.API.Gallery.Visibility, b GHC.Types.~ Web.Exhentai.API.Gallery.Visibility) => Optics.Internal.Optic.LabelOptic "visibility" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ [(Web.Exhentai.API.Gallery.TagCategory, [Data.Text.Internal.Text])], b GHC.Types.~ [(Web.Exhentai.API.Gallery.TagCategory, [Data.Text.Internal.Text])]) => Optics.Internal.Optic.LabelOptic "tags" k Web.Exhentai.API.Gallery.GalleryInfo Web.Exhentai.API.Gallery.GalleryInfo a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Artist" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_ArtistCG" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_AsianPorn" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Character" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Cosplay" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Doujinshi" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Expunged" k Web.Exhentai.API.Gallery.Visibility Web.Exhentai.API.Gallery.Visibility a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Female" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_GameCG" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Group" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_ImageSet" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Language" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Male" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Manga" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Misc" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Misc'" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_NonH" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Parody" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Private" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Reclass" k Web.Exhentai.API.Gallery.TagCategory Web.Exhentai.API.Gallery.TagCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Replaced" k Web.Exhentai.API.Gallery.Visibility Web.Exhentai.API.Gallery.Visibility a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Visible" k Web.Exhentai.API.Gallery.Visibility Web.Exhentai.API.Gallery.Visibility a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ (), b GHC.Types.~ ()) => Optics.Internal.Optic.LabelOptic "_Western" k Web.Exhentai.API.Gallery.GalleryCategory Web.Exhentai.API.Gallery.GalleryCategory a b
+ Web.Exhentai.API.Gallery: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "_Unknown" k Web.Exhentai.API.Gallery.Visibility Web.Exhentai.API.Gallery.Visibility a b
+ Web.Exhentai.API.Gallery: instance GHC.Classes.Eq Web.Exhentai.API.Gallery.Gallery
+ Web.Exhentai.API.Gallery: instance GHC.Classes.Eq Web.Exhentai.API.Gallery.GalleryCategory
+ Web.Exhentai.API.Gallery: instance GHC.Classes.Eq Web.Exhentai.API.Gallery.TagCategory
+ Web.Exhentai.API.Gallery: instance GHC.Classes.Ord Web.Exhentai.API.Gallery.GalleryCategory
+ Web.Exhentai.API.Gallery: instance GHC.Enum.Bounded Web.Exhentai.API.Gallery.GalleryCategory
+ Web.Exhentai.API.Gallery: instance GHC.Enum.Enum Web.Exhentai.API.Gallery.GalleryCategory
+ Web.Exhentai.API.Gallery: instance GHC.Enum.Enum Web.Exhentai.API.Gallery.TagCategory
+ Web.Exhentai.API.Gallery: instance GHC.Show.Show Web.Exhentai.API.Gallery.Gallery
+ Web.Exhentai.API.Gallery: instance GHC.Show.Show Web.Exhentai.API.Gallery.GalleryCategory
+ Web.Exhentai.API.Gallery: instance GHC.Show.Show Web.Exhentai.API.Gallery.TagCategory
+ Web.Exhentai.API.Gallery: parseGalleryLink :: Text -> Maybe Gallery
+ Web.Exhentai.API.MPV: buildRequest :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => Gallery -> m [DispatchRequest]
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "dimension" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "galleryPath" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "imgKey" k Web.Exhentai.API.MPV.DispatchRequest Web.Exhentai.API.MPV.DispatchRequest a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "imgLink" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "mpvKey" k Web.Exhentai.API.MPV.DispatchRequest Web.Exhentai.API.MPV.DispatchRequest a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "origImgPath" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "searchPath" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe Web.Exhentai.API.MPV.Server, b GHC.Types.~ GHC.Maybe.Maybe Web.Exhentai.API.MPV.Server) => Optics.Internal.Optic.LabelOptic "exclude" k Web.Exhentai.API.MPV.DispatchRequest Web.Exhentai.API.MPV.DispatchRequest a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "galleryId" k Web.Exhentai.API.MPV.DispatchRequest Web.Exhentai.API.MPV.DispatchRequest a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "page" k Web.Exhentai.API.MPV.DispatchRequest Web.Exhentai.API.MPV.DispatchRequest a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Web.Exhentai.API.MPV.Dim, b GHC.Types.~ Web.Exhentai.API.MPV.Dim) => Optics.Internal.Optic.LabelOptic "height" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Web.Exhentai.API.MPV.Dim, b GHC.Types.~ Web.Exhentai.API.MPV.Dim) => Optics.Internal.Optic.LabelOptic "width" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Web.Exhentai.API.MPV.Server, b GHC.Types.~ Web.Exhentai.API.MPV.Server) => Optics.Internal.Optic.LabelOptic "server" k Web.Exhentai.API.MPV.DispatchResult Web.Exhentai.API.MPV.DispatchResult a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "_Other" k Web.Exhentai.API.MPV.Server Web.Exhentai.API.MPV.Server a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Prism, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "_HAtH" k Web.Exhentai.API.MPV.Server Web.Exhentai.API.MPV.Server a b
+ Web.Exhentai.API.MPV: instance (k GHC.Types.~ Optics.Internal.Optic.Types.An_Iso, a GHC.Types.~ GHC.Types.Int, b GHC.Types.~ GHC.Types.Int) => Optics.Internal.Optic.LabelOptic "_Dim" k Web.Exhentai.API.MPV.Dim Web.Exhentai.API.MPV.Dim a b
+ Web.Exhentai.API.Search: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ Data.Text.Internal.Text, b GHC.Types.~ Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "searchString" k Web.Exhentai.API.Search.SearchQuery Web.Exhentai.API.Search.SearchQuery a b
+ Web.Exhentai.API.Search: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe (Data.Set.Internal.Set Web.Exhentai.API.Gallery.GalleryCategory), b GHC.Types.~ GHC.Maybe.Maybe (Data.Set.Internal.Set Web.Exhentai.API.Gallery.GalleryCategory)) => Optics.Internal.Optic.LabelOptic "categories" k Web.Exhentai.API.Search.SearchQuery Web.Exhentai.API.Search.SearchQuery a b
+ Web.Exhentai.API.Search: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe Data.Text.Internal.Text, b GHC.Types.~ GHC.Maybe.Maybe Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "nextPage" k Web.Exhentai.API.Search.SearchResult Web.Exhentai.API.Search.SearchResult a b
+ Web.Exhentai.API.Search: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ GHC.Maybe.Maybe Data.Text.Internal.Text, b GHC.Types.~ GHC.Maybe.Maybe Data.Text.Internal.Text) => Optics.Internal.Optic.LabelOptic "prevPage" k Web.Exhentai.API.Search.SearchResult Web.Exhentai.API.Search.SearchResult a b
+ Web.Exhentai.API.Search: instance (k GHC.Types.~ Optics.Internal.Optic.Types.A_Lens, a GHC.Types.~ [Web.Exhentai.API.Gallery.Gallery], b GHC.Types.~ [Web.Exhentai.API.Gallery.Gallery]) => Optics.Internal.Optic.LabelOptic "galleries" k Web.Exhentai.API.Search.SearchResult Web.Exhentai.API.Search.SearchResult a b
+ Web.Exhentai.Parsing.Image: nextImage :: Traversal' Element Text
+ Web.Exhentai.Utils: (!!) :: [a] -> Int -> Maybe a
- Web.Exhentai.API.Archiver: streamOriginal :: (MonadHttpState m, MonadIO n) => Text -> ContT r m (Response (ConduitT i ByteString n ()))
+ Web.Exhentai.API.Archiver: streamOriginal :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => Text -> ContT r m (Response (ConduitT i ByteString IO ()))
- Web.Exhentai.API.Archiver: streamResampled :: (MonadHttpState m, MonadIO n) => Text -> ContT r m (Response (ConduitT i ByteString n ()))
+ Web.Exhentai.API.Archiver: streamResampled :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => Text -> ContT r m (Response (ConduitT i ByteString IO ()))
- Web.Exhentai.API.Auth: auth :: MonadHttpState m => Credential -> m ()
+ Web.Exhentai.API.Auth: auth :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket] m => Credential -> m ()
- Web.Exhentai.API.Gallery: GalleryInfo :: {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> GalleryCat -> Maybe Text -> {-# UNPACK #-}Text -> {-# UNPACK #-}Float -> {-# UNPACK #-}Int -> {-# UNPACK #-}Int -> [(TagCategory, [Text])] -> {-# UNPACK #-}UTCTime -> Maybe Gallery -> Maybe Gallery -> Visibility -> {-# UNPACK #-}Text -> {-# UNPACK #-}Int -> {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> GalleryInfo
+ Web.Exhentai.API.Gallery: GalleryInfo :: {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> GalleryCategory -> Maybe Text -> {-# UNPACK #-}Text -> {-# UNPACK #-}Double -> {-# UNPACK #-}Int -> {-# UNPACK #-}Int -> [(TagCategory, [Text])] -> {-# UNPACK #-}UTCTime -> Maybe Gallery -> Maybe Gallery -> Visibility -> {-# UNPACK #-}Text -> {-# UNPACK #-}Int -> {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> GalleryInfo
- Web.Exhentai.API.Gallery: [$sel:category:GalleryInfo] :: GalleryInfo -> GalleryCat
+ Web.Exhentai.API.Gallery: [$sel:category:GalleryInfo] :: GalleryInfo -> GalleryCategory
- Web.Exhentai.API.Gallery: [$sel:rating:GalleryInfo] :: GalleryInfo -> {-# UNPACK #-}Float
+ Web.Exhentai.API.Gallery: [$sel:rating:GalleryInfo] :: GalleryInfo -> {-# UNPACK #-}Double
- Web.Exhentai.API.Gallery: fetchGalleryInfo :: MonadHttpState m => Gallery -> m GalleryInfo
+ Web.Exhentai.API.Gallery: fetchGalleryInfo :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => Gallery -> m GalleryInfo
- Web.Exhentai.API.MPV: DispatchRequest :: Int -> Int -> Text -> Text -> Maybe Server -> DispatchRequest
+ Web.Exhentai.API.MPV: DispatchRequest :: {-# UNPACK #-}Int -> {-# UNPACK #-}Int -> {-# UNPACK #-}Text -> {-# UNPACK #-}Text -> Maybe Server -> DispatchRequest
- Web.Exhentai.API.MPV: HAtH :: Int -> Server
+ Web.Exhentai.API.MPV: HAtH :: {-# UNPACK #-}Int -> Server
- Web.Exhentai.API.MPV: Other :: Text -> Server
+ Web.Exhentai.API.MPV: Other :: {-# UNPACK #-}Text -> Server
- Web.Exhentai.API.MPV: [$sel:galleryId:DispatchRequest] :: DispatchRequest -> Int
+ Web.Exhentai.API.MPV: [$sel:galleryId:DispatchRequest] :: DispatchRequest -> {-# UNPACK #-}Int
- Web.Exhentai.API.MPV: [$sel:imgKey:DispatchRequest] :: DispatchRequest -> Text
+ Web.Exhentai.API.MPV: [$sel:imgKey:DispatchRequest] :: DispatchRequest -> {-# UNPACK #-}Text
- Web.Exhentai.API.MPV: [$sel:mpvKey:DispatchRequest] :: DispatchRequest -> Text
+ Web.Exhentai.API.MPV: [$sel:mpvKey:DispatchRequest] :: DispatchRequest -> {-# UNPACK #-}Text
- Web.Exhentai.API.MPV: [$sel:page:DispatchRequest] :: DispatchRequest -> Int
+ Web.Exhentai.API.MPV: [$sel:page:DispatchRequest] :: DispatchRequest -> {-# UNPACK #-}Int
- Web.Exhentai.API.MPV: fetchImage :: (MonadHttpState m, MonadIO n) => DispatchRequest -> ContT r m (Response (ConduitT i ByteString n ()))
+ Web.Exhentai.API.MPV: fetchImage :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => DispatchRequest -> ContT r m (Response (ConduitT i ByteString IO ()))
- Web.Exhentai.API.Search: SearchQuery :: Maybe (Set GalleryCat) -> {-# UNPACK #-}Text -> SearchQuery
+ Web.Exhentai.API.Search: SearchQuery :: Maybe (Set GalleryCategory) -> {-# UNPACK #-}Text -> SearchQuery
- Web.Exhentai.API.Search: [$sel:categories:SearchQuery] :: SearchQuery -> Maybe (Set GalleryCat)
+ Web.Exhentai.API.Search: [$sel:categories:SearchQuery] :: SearchQuery -> Maybe (Set GalleryCategory)
- Web.Exhentai.API.Search: fetchSearchPage :: MonadHttpState m => Text -> m SearchResult
+ Web.Exhentai.API.Search: fetchSearchPage :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => Text -> m SearchResult
- Web.Exhentai.API.Search: search :: MonadHttpState m => SearchQuery -> m SearchResult
+ Web.Exhentai.API.Search: search :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => SearchQuery -> m SearchResult
- Web.Exhentai.API.Search: searchRecur :: MonadHttpState m => SearchQuery -> ConduitT i Gallery m ()
+ Web.Exhentai.API.Search: searchRecur :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => SearchQuery -> ConduitT i Gallery m ()
- Web.Exhentai.API.Search: searchRecurResumable :: MonadHttpState m => SearchQuery -> ConduitT i (Either Text Gallery) m ()
+ Web.Exhentai.API.Search: searchRecurResumable :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => SearchQuery -> ConduitT i (Either Text Gallery) m ()
- Web.Exhentai.API.Watched: fetchPopular :: MonadHttpState m => m [Gallery]
+ Web.Exhentai.API.Watched: fetchPopular :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket] m => m [Gallery]
- Web.Exhentai.API.Watched: fetchWatched :: MonadHttpState m => m [Gallery]
+ Web.Exhentai.API.Watched: fetchWatched :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket] m => m [Gallery]
- Web.Exhentai.Parsing.Gallery: averageRating :: Traversal' Element AverageRating
+ Web.Exhentai.Parsing.Gallery: averageRating :: Traversal' Element Text
- Web.Exhentai.Parsing.Gallery: category :: Traversal' Element GalleryCat
+ Web.Exhentai.Parsing.Gallery: category :: Traversal' Element Text
- Web.Exhentai.Parsing.Gallery: meta :: Traversal' Element Element
+ Web.Exhentai.Parsing.Gallery: meta :: AffineTraversal' Element Element
- Web.Exhentai.Parsing.Gallery: newer :: Traversal' Element Gallery
+ Web.Exhentai.Parsing.Gallery: newer :: Traversal' Element Text
- Web.Exhentai.Parsing.Gallery: parent :: Traversal' Element Gallery
+ Web.Exhentai.Parsing.Gallery: parent :: Traversal' Element Text
- Web.Exhentai.Parsing.Gallery: popupLink :: Traversal' Element PopUpLink
+ Web.Exhentai.Parsing.Gallery: popupLink :: Traversal' Element Text
- Web.Exhentai.Parsing.Gallery: tagCategory :: Traversal' Element TagCategory
+ Web.Exhentai.Parsing.Gallery: tagCategory :: Traversal' Element Text
- Web.Exhentai.Parsing.Search: galleryLink :: Traversal' Element Gallery
+ Web.Exhentai.Parsing.Search: galleryLink :: Traversal' Element Text
- Web.Exhentai.Utils: (^..:) :: Document -> Fold Element a -> [a]
+ Web.Exhentai.Utils: (^..:) :: (Is (Join A_Traversal l) A_Fold, Is l (Join A_Traversal l), Is A_Traversal (Join A_Traversal l)) => Document -> Optic l is Element Element a a -> [a]
- Web.Exhentai.Utils: (^?:) :: Document -> Fold Element a -> Maybe a
+ Web.Exhentai.Utils: (^?:) :: (Is (Join A_Traversal l) A_Fold, Is l (Join A_Traversal l), Is A_Traversal (Join A_Traversal l)) => Document -> Optic l is Element Element a a -> Maybe a
- Web.Exhentai.Utils: a :: Traversal' Element Element
+ Web.Exhentai.Utils: a :: AffineTraversal' Element Element
- Web.Exhentai.Utils: cl :: Text -> Traversal' Element Element
+ Web.Exhentai.Utils: cl :: Text -> AffineTraversal' Element Element
- Web.Exhentai.Utils: div :: Traversal' Element Element
+ Web.Exhentai.Utils: div :: AffineTraversal' Element Element
- Web.Exhentai.Utils: h1 :: Traversal' Element Element
+ Web.Exhentai.Utils: h1 :: AffineTraversal' Element Element
- Web.Exhentai.Utils: htmlRequest :: MonadHttpState m => Request -> m Document
+ Web.Exhentai.Utils: htmlRequest :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => Request -> m Document
- Web.Exhentai.Utils: htmlRequest' :: MonadHttpState m => Text -> m Document
+ Web.Exhentai.Utils: htmlRequest' :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => Text -> m Document
- Web.Exhentai.Utils: id :: Text -> Traversal' Element Element
+ Web.Exhentai.Utils: id :: Text -> AffineTraversal' Element Element
- Web.Exhentai.Utils: img :: Traversal' Element Element
+ Web.Exhentai.Utils: img :: AffineTraversal' Element Element
- Web.Exhentai.Utils: jsonRequest :: (FromJSON a, MonadHttpState m) => Request -> m (Either String a)
+ Web.Exhentai.Utils: jsonRequest :: (FromJSON a, Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m) => Request -> m (Either String a)
- Web.Exhentai.Utils: scripts :: Traversal' Element Element
+ Web.Exhentai.Utils: scripts :: AffineTraversal' Element Element
- Web.Exhentai.Utils: table :: Traversal' Element Element
+ Web.Exhentai.Utils: table :: AffineTraversal' Element Element
- Web.Exhentai.Utils: td :: Traversal' Element Element
+ Web.Exhentai.Utils: td :: AffineTraversal' Element Element
- Web.Exhentai.Utils: tr :: Traversal' Element Element
+ Web.Exhentai.Utils: tr :: AffineTraversal' Element Element
Files
- CHANGELOG.md +4/−0
- exh.cabal +24/−29
- src/Control/Effect/Exh.hs +195/−0
- src/Language/JavaScript/Extraction.hs +155/−0
- src/Web/Exhentai.hs +36/−1
- src/Web/Exhentai/API/Archiver.hs +30/−21
- src/Web/Exhentai/API/Auth.hs +19/−9
- src/Web/Exhentai/API/Gallery.hs +199/−29
- src/Web/Exhentai/API/MPV.hs +85/−41
- src/Web/Exhentai/API/Search.hs +58/−27
- src/Web/Exhentai/API/Watched.hs +14/−6
- src/Web/Exhentai/Errors.hs +2/−3
- src/Web/Exhentai/Parsing/Gallery.hs +59/−72
- src/Web/Exhentai/Parsing/Image.hs +8/−5
- src/Web/Exhentai/Parsing/MPV.hs +0/−55
- src/Web/Exhentai/Parsing/Search.hs +18/−14
- src/Web/Exhentai/Types.hs +0/−187
- src/Web/Exhentai/Types/CookieT.hs +0/−180
- src/Web/Exhentai/Utils.hs +61/−36
- test/Spec.hs +3/−24
CHANGELOG.md view
@@ -8,3 +8,7 @@ - Bug fixes - Add a resumable variant of the search function++## 1.0.0++- Rewrite using in-other-words
exh.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: exh-version: 0.2.0+version: 1.0.0 synopsis: A library for crawling exhentai description: A library for crawling exhentai, with the support of streaming@@ -11,7 +11,7 @@ author: Poscat maintainer: Poscat <poscat@mail.poscat.moe> copyright: Copyright (c) Poscat 2021-stability: alpha+stability: experimental homepage: https://github.com/poscat0x04/exh bug-reports: https://github.com/poscat0x04/exh/issues extra-doc-files:@@ -20,29 +20,23 @@ common common-attrs build-depends:- , aeson >=1.4.7 && <1.6- , base >=4.10 && <5- , bytestring >=0.10.12 && <0.12- , conduit ^>=1.3.4- , containers ^>=0.6.2- , exceptions ^>=0.10.4- , html-conduit ^>=1.3.2- , http-client >=0.6.4 && <0.8- , http-client-tls ^>=0.3.5- , http-conduit ^>=2.3.7- , lens ^>=4.19- , megaparsec ^>=9.0- , monad-control ^>=1.0.2- , monad-time ^>=0.3- , mtl ^>=2.2- , quickjs-hs ^>=0.1.2.4- , retry ^>=0.8.1- , text ^>=1.2.3- , time >=1.9.3 && <1.12- , transformers ^>=0.5.6- , transformers-base ^>=0.4.5- , xml-conduit >=1.8.0 && <1.10- , xml-lens >=0.2 && <0.3+ , aeson >=1.4.7 && <1.6+ , base >=4.10 && <5+ , bytestring >=0.10.12 && <0.12+ , conduit ^>=1.3.4+ , containers ^>=0.6.2+ , html-conduit ^>=1.3.2+ , http-client >=0.6.4 && <0.8+ , in-other-words+ , language-javascript+ , megaparsec ^>=9.0+ , optics-core+ , optics-th+ , text ^>=1.2.3+ , time >=1.9.3 && <1.12+ , transformers+ , xml-conduit >=1.8.0 && <1.10+ , xml-optics default-language: Haskell2010 default-extensions:@@ -86,16 +80,16 @@ Web.Exhentai.API.MPV Web.Exhentai.API.Search Web.Exhentai.API.Watched- Web.Exhentai.Errors Web.Exhentai.Parsing.Gallery Web.Exhentai.Parsing.Image- Web.Exhentai.Parsing.MPV Web.Exhentai.Parsing.Search- Web.Exhentai.Types- Web.Exhentai.Types.CookieT Web.Exhentai.Utils other-modules:+ Control.Effect.Exh+ Language.JavaScript.Extraction+ Web.Exhentai.Errors+ hs-source-dirs: src test-suite exh-test@@ -104,6 +98,7 @@ build-depends: , exh , hspec+ , http-client-tls hs-source-dirs: test main-is: Spec.hs
+ src/Control/Effect/Exh.hs view
@@ -0,0 +1,195 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Internal module+module Control.Effect.Exh where++import Conduit+import Control.Concurrent+ ( MVar,+ newMVar,+ putMVar,+ readMVar,+ takeMVar,+ )+import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Reader+import Control.Monad+import Control.Monad.Trans.Cont+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Network.HTTP.Client hiding (Cookie)+import Network.HTTP.Client.MultipartFormData++data Http :: Effect where+ FormRequest :: String -> Http m Request+ GenBoundary :: Http m ByteString+ RespOpen :: Request -> Http m (Response BodyReader)+ RespClose :: Response a -> Http m ()++formRequest :: Effs '[Http, Error HttpException] m => String -> m Request+formRequest = send . FormRequest+{-# INLINE formRequest #-}++genBoundary :: Eff Http m => m ByteString+genBoundary = send GenBoundary+{-# INLINE genBoundary #-}++respOpen :: Effs '[Http, Error HttpException] m => Request -> m (Response BodyReader)+respOpen = send . RespOpen+{-# INLINE respOpen #-}++respClose :: Eff Http m => Response a -> m ()+respClose = send . RespClose+{-# INLINE respClose #-}++data HttpH++instance (Effs '[Embed IO, Reader Manager] m) => Handler HttpH Http m where+ effHandler (FormRequest s) = embed @IO $ parseRequest s+ effHandler GenBoundary = embed webkitBoundary+ effHandler (RespOpen req) = ask >>= embed . responseOpen req+ effHandler (RespClose resp) = embed $ responseClose resp+ {-# INLINEABLE effHandler #-}++type HttpToIOC = InterpretC HttpH Http++httpToIO :: (Effs '[Embed IO, Reader Manager] m, Threaders '[ReaderThreads] m p) => HttpToIOC m a -> m a+httpToIO = interpretViaHandler+{-# INLINE httpToIO #-}++--------------------------------------------------+--++data Cookie :: Effect where+ TakeCookie :: Cookie m CookieJar+ ReadCookie :: Cookie m CookieJar+ PutCookie :: CookieJar -> Cookie m ()++takeCookie :: Eff Cookie m => m CookieJar+takeCookie = send TakeCookie+{-# INLINE takeCookie #-}++readCookie :: Eff Cookie m => m CookieJar+readCookie = send ReadCookie+{-# INLINE readCookie #-}++putCookie :: Eff Cookie m => CookieJar -> m ()+putCookie = send . PutCookie+{-# INLINE putCookie #-}++data CookieH++instance Effs '[Reader (MVar CookieJar), Embed IO] m => Handler CookieH Cookie m where+ effHandler TakeCookie = ask >>= embed . takeMVar+ effHandler ReadCookie = ask >>= embed . readMVar+ effHandler (PutCookie c) = do+ ref <- ask+ embed $ putMVar ref c+ {-# INLINEABLE effHandler #-}++type CookieToReaderC = InterpretC CookieH Cookie++type CookieToIOC = CompositionC '[CookieToReaderC, ReaderC (MVar CookieJar)]++cookieToIO :: (Eff (Embed IO) m, Threaders '[ReaderThreads] m p) => CookieToIOC m a -> m a+cookieToIO m = do+ ref <- embed $ newMVar mempty+ runReader ref $+ interpretViaHandler $+ runComposition m+{-# INLINE cookieToIO #-}++--------------------------------------------------+--++data ConduitIO :: Effect where+ RunConduitIO :: ConduitT () Void IO a -> ConduitIO m a++runConduitIO :: Eff ConduitIO m => ConduitT () Void IO a -> m a+runConduitIO = send . RunConduitIO+{-# INLINE runConduitIO #-}++data ConduitIOH++instance Eff (Embed IO) m => Handler ConduitIOH ConduitIO m where+ effHandler (RunConduitIO c) = embed $ runConduit c+ {-# INLINEABLE effHandler #-}++type ConduitIOToIOC = InterpretC ConduitIOH ConduitIO++conduitIOToIO :: Eff (Embed IO) m => ConduitIOToIOC m a -> m a+conduitIOToIO = interpretViaHandler+{-# INLINE conduitIOToIO #-}++--------------------------------------------------+--++type ExhC = CompositionC '[HttpToIOC, ConduitIOToIOC, CookieToIOC]++exhToIO :: (Effs '[Embed IO, Reader Manager] m, Threaders '[ReaderThreads] m p) => ExhC m a -> m a+exhToIO m =+ cookieToIO $+ conduitIOToIO $+ httpToIO $+ runComposition m+{-# INLINE exhToIO #-}++--------------------------------------------------+--++attachFormData :: Eff Http m => [PartM m] -> Request -> m Request+attachFormData parts req = do+ boundary <- genBoundary+ formDataBodyWithBoundary boundary parts req++bodyReaderSource :: BodyReader -> ConduitT i ByteString IO ()+bodyReaderSource br = loop+ where+ loop = do+ bs <- lift br+ unless (B.null bs) $ do+ yield bs+ loop++resetCookie :: Eff Cookie m => m ()+resetCookie = takeCookie >> putCookie mempty+{-# INLINEABLE resetCookie #-}++modifyJar ::+ Effs '[Http, Cookie, Error HttpException, Bracket] m =>+ Request ->+ m ()+modifyJar req =+ bracketOnError+ takeCookie+ putCookie+ $ \jar -> do+ let req' = req {cookieJar = Just jar}+ bracket+ (respOpen req')+ respClose+ (putCookie . responseCookieJar)+{-# INLINEABLE modifyJar #-}++openWithJar ::+ Effs '[Http, Cookie, Error HttpException] m => Request -> m (Response (ConduitT i ByteString IO ()))+openWithJar req = do+ jar <- readCookie+ resp <- respOpen (req {cookieJar = Just jar})+ pure $ fmap bodyReaderSource resp++withSource ::+ Effs '[Http, Cookie, Error HttpException, Bracket] m =>+ Request ->+ ContT r m (Response (ConduitT i ByteString IO ()))+withSource req = ContT $ \k -> do+ jar <- readCookie+ bracket+ (respOpen (req {cookieJar = Just jar}))+ respClose+ (k . fmap bodyReaderSource)+{-# INLINEABLE withSource #-}
+ src/Language/JavaScript/Extraction.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Internal module. Extract values from mpv script+module Language.JavaScript.Extraction where++import Data.Aeson+import Data.ByteString.Lazy.Char8 (pack)+import Data.Maybe+import Data.Text (Text)+import Language.JavaScript.Parser.AST+import Optics.TH+import Text.Read+import Web.Exhentai.Utils hiding (div)+import Prelude hiding ((!!))++data MpvImage = MpvImage+ { name :: {-# UNPACK #-} Text,+ key :: {-# UNPACK #-} Text,+ thumbnail :: {-# UNPACK #-} Text+ }+ deriving (Show, Eq)++instance FromJSON MpvImage where+ parseJSON = withObject "mpv image" $ \o ->+ MpvImage+ <$> o .: "n"+ <*> o .: "k"+ <*> o .: "t"++-- | All the variables defined in the scripts that came with the MPV+data Vars = Vars+ { gid :: {-# UNPACK #-} Int,+ mpvkey :: {-# UNPACK #-} Text,+ pageCount :: {-# UNPACK #-} Int,+ imageList :: [MpvImage]+ }+ deriving (Show, Eq)++class As a b where+ as :: a -> Maybe b++instance As JSExpression Int where+ as (JSDecimal _ s) = decode (pack s)+ as (JSExpressionBinary expr1 op expr2) = do+ i1 <- as expr1+ i2 <- as expr2+ case op of+ JSBinOpPlus {} -> pure $ i1 + i2+ JSBinOpMinus {} -> pure $ i1 - i2+ JSBinOpTimes {} -> pure $ i1 * i2+ JSBinOpDivide {} -> pure $ i1 `div` i2+ _ -> Nothing+ as (JSUnaryExpression op expr) = do+ i <- as expr+ case op of+ JSUnaryOpIncr {} -> pure $ i + 1+ JSUnaryOpDecr {} -> pure $ i - 1+ JSUnaryOpMinus {} -> pure $ - i+ JSUnaryOpPlus {} -> pure i+ _ -> Nothing+ as (JSVarInitExpression _ initializer) = as initializer+ as _ = Nothing++instance As JSExpression a => As JSVarInitializer a where+ as (JSVarInit _ expr) = as expr+ as _ = Nothing++instance As (JSCommaList JSExpression) a => As JSStatement a where+ as (JSVariable _ l _) = as l+ as _ = Nothing++instance {-# OVERLAPPABLE #-} As a b => As (JSCommaList a) b where+ as (JSLOne x) = as x+ as (JSLCons _ _ x) = as x+ as _ = Nothing++instance As JSExpression Text where+ as (JSStringLiteral _ s) = decode (pack s)+ as (JSVarInitExpression _ initializer) = as initializer+ as _ = Nothing++instance As JSPropertyName Text where+ as (JSPropertyString _ s) = readMaybe s+ as _ = Nothing++instance As JSArrayElement a => As JSExpression [a] where+ as (JSArrayLiteral _ l _) = Just $ mapMaybe as l+ as (JSVarInitExpression _ initializer) = as initializer+ as _ = Nothing++instance As JSExpression a => As JSArrayElement a where+ as (JSArrayElement expr) = as expr+ as (JSArrayComma _) = Nothing++class AsPair a where+ asPair :: a -> Maybe (Text, Text)++instance AsPair JSObjectProperty where+ asPair (JSPropertyNameandValue n _ [expr]) = (,) <$> as n <*> as expr+ asPair _ = Nothing++instance As (JSCommaList JSObjectProperty) [(Text, Text)] where+ as (JSLCons xs _ p) = (:) <$> asPair p <*> as xs+ as (JSLOne p) = pure <$> asPair p+ as JSLNil = pure []++instance {-# OVERLAPPABLE #-} As (JSCommaList a) b => As (JSCommaTrailingList a) b where+ as (JSCTLComma l _) = as l+ as (JSCTLNone l) = as l++instance As JSObjectPropertyList MpvImage where+ as l = do+ mapping <- (as l :: Maybe [(Text, Text)])+ thumbnail <- lookup "t" mapping+ key <- lookup "k" mapping+ name <- lookup "n" mapping+ pure MpvImage {..}++instance As JSExpression MpvImage where+ as (JSObjectLiteral _ l _) = as l+ as _ = Nothing++instance As JSAST Vars where+ as (JSAstProgram stmts _) = do+ gidStmt <- stmts !! 1+ gid <- as gidStmt+ imgStmt <- stmts !! 3+ imageList <- as imgStmt+ keyStmt <- stmts !! 2+ mpvkey <- as keyStmt+ pgCountStmt <- stmts !! 9+ pageCount <- as pgCountStmt+ pure Vars {..}+ as _ = Nothing++makeFieldLabelsWith noPrefixFieldLabels ''Vars+makeFieldLabelsWith noPrefixFieldLabels ''MpvImage++{-+extractEnv :: Text -> IO (Result Vars)+extractEnv script = quickjs $ do+ eval_ $ encodeUtf8 script+ gid' <- eval "gid"+ mpvkey' <- eval "mpvkey"+ imageList' <- eval "imagelist"+ pageCount' <- eval "pagecount"+ pure $ do+ gid <- fromJSON gid'+ mpvkey <- fromJSON mpvkey'+ imageList <- fromJSON imageList'+ pageCount <- fromJSON pageCount'+ pure Vars {..}+-}
src/Web/Exhentai.hs view
@@ -1,5 +1,35 @@ module Web.Exhentai- ( -- ** Authentication+ ( -- * Effects and effect interpreters++ -- ** Important+ Http,+ formRequest,+ genBoundary,+ respOpen,+ respClose,+ ConduitIO (..),+ runConduitIO,+ Cookie (..),+ takeCookie,+ readCookie,+ putCookie,+ ExhC,+ exhToIO,++ -- ** Unimportant+ HttpH,+ HttpToIOC,+ httpToIO,+ CookieH,+ CookieToIOC,+ cookieToIO,+ ConduitIOH,+ ConduitIOToIOC,+ conduitIOToIO,++ -- * API++ -- ** Authentication module Au, -- ** Getting information about galleries@@ -16,12 +46,17 @@ -- ** Fetching archives module Ar,++ -- * Errors+ ExhentaiError (..), ) where +import Control.Effect.Exh import Web.Exhentai.API.Archiver as Ar import Web.Exhentai.API.Auth as Au import Web.Exhentai.API.Gallery as G import Web.Exhentai.API.MPV as M import Web.Exhentai.API.Search as S import Web.Exhentai.API.Watched as W+import Web.Exhentai.Errors
src/Web/Exhentai/API/Archiver.hs view
@@ -5,64 +5,73 @@ where import Conduit-import Control.Lens (Traversal')-import Control.Monad.Catch-import Control.Monad.Cont+import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Exh+import Control.Monad.Trans.Cont import Data.ByteString (ByteString) import Data.Text (Text, unpack)-import Network.HTTP.Client.Conduit+import Network.HTTP.Client hiding (Cookie) import Network.HTTP.Client.MultipartFormData-import Text.XML.Lens+import Optics.Core (Traversal')+import Text.XML.Optics import Web.Exhentai.Errors-import Web.Exhentai.Types.CookieT import Web.Exhentai.Utils import Prelude hiding (id) downloadLink :: Traversal' Element Text-downloadLink = id "db" ... id "continue" ... attr "href"+downloadLink = id "db" .// id "continue" .// attr "href"+{-# INLINE downloadLink #-} -originalParts :: [Part]+originalParts :: Applicative m => [PartM m] originalParts = [ partBS "dltype" "org", partBS "dlcheck" "Download Original Archive" ] {-# INLINE originalParts #-} -resampledParts :: [Part]+resampledParts :: Applicative m => [PartM m] resampledParts = [ partBS "dltype" "res", partBS "dlcheck" "Download Resample Archive" ] {-# INLINE resampledParts #-} -streamWith :: (MonadHttpState m, MonadIO n) => [Part] -> Text -> ContT r m (Response (ConduitT i ByteString n ()))+streamWith ::+ Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>+ [PartM m] ->+ Text ->+ ContT r m (Response (ConduitT i ByteString IO ())) streamWith parts url = ContT $ \k -> do initReq <- formRequest $ unpack url- req <- formDataBody parts initReq+ req <- attachFormData parts initReq d <- htmlRequest req case d ^?: downloadLink of- Nothing -> throwM $ XMLParseFailure "download link" url+ Nothing -> throw $ XMLParseFailure "download link" url Just l -> do newReq <- formRequest $ unpack l let req' = setQueryString [("start", Just "1")] newReq- retryWhenTimeout $- bracket- (respOpen req')- respClose- k+ bracket+ (respOpen req')+ respClose+ (k . fmap bodyReaderSource)+{-# INLINEABLE streamWith #-} -- | Download an origian archive from an archiver url as a stream streamOriginal ::- (MonadHttpState m, MonadIO n) =>+ Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => -- | Archiver url, usually the 'archiverLink` field Text ->- ContT r m (Response (ConduitT i ByteString n ()))+ ContT r m (Response (ConduitT i ByteString IO ())) streamOriginal = streamWith originalParts+{-# INLINEABLE streamOriginal #-} -- | Download an resampled archive from an archiver url as a stream streamResampled ::- (MonadHttpState m, MonadIO n) =>+ Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m => -- | Archiver url, usually the 'archiverLink` field Text ->- ContT r m (Response (ConduitT i ByteString n ()))+ ContT r m (Response (ConduitT i ByteString IO ())) streamResampled = streamWith resampledParts+{-# INLINEABLE streamResampled #-}
src/Web/Exhentai/API/Auth.hs view
@@ -1,24 +1,31 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Authentication API. module Web.Exhentai.API.Auth ( Credential (..), auth, ) where +import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Exh import Data.ByteString (ByteString)-import GHC.Generics-import Network.HTTP.Client.Conduit+import Network.HTTP.Client hiding (Cookie) import Network.HTTP.Client.MultipartFormData-import Web.Exhentai.Types.CookieT+import Optics.TH data Credential = Credential { username :: ByteString, password :: ByteString }- deriving (Show, Eq, Generic)+ deriving (Show, Eq) -- | Authenticates and loads user preferences. -- This should be called before any other functions are called-auth :: MonadHttpState m => Credential -> m ()+auth :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket] m => Credential -> m () auth Credential {..} = do initReq <- formRequest "POST https://forums.e-hentai.org/index.php" let parts =@@ -36,10 +43,13 @@ ] initReq finalReq <- attachFormData parts req- modifyingJar finalReq+ modifyJar finalReq req2 <- formRequest "https://exhentai.org"- modifyingJar req2+ modifyJar req2 req3 <- formRequest "https://exhentai.org/uconfig.php"- modifyingJar req3+ modifyJar req3 req4 <- formRequest "https://exhentai.org/mytags"- modifyingJar req4+ modifyJar req4+{-# INLINEABLE auth #-}++makeFieldLabelsWith noPrefixFieldLabels ''Credential
src/Web/Exhentai/API/Gallery.hs view
@@ -1,41 +1,133 @@ {-# LANGUAGE StrictData #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE UndecidableInstances #-} +-- | Gallery API. module Web.Exhentai.API.Gallery- ( GalleryInfo (..),+ ( Gallery (..),+ GalleryInfo (..), Visibility (..),+ GalleryCategory (..),+ TagCategory (..),+ allGalleryCats, fetchGalleryInfo,++ -- ** Internal API+ parseGalleryLink, parseGallery, ) where -import Control.Lens ((^..), (^?))+import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Exh import Control.Monad-import Control.Monad.Catch-import Data.Coerce-import Data.Text (Text, strip)+import Data.Maybe+import Data.Set (Set, fromList)+import Data.Text (Text, pack, strip) import Data.Time-import GHC.Generics+import Data.Void+import Network.HTTP.Client hiding (Cookie)+import Optics.Core+import Optics.TH+import Text.Megaparsec hiding (token)+import Text.Megaparsec.Char.Lexer import Text.XML-import Text.XML.Lens+import Text.XML.Optics import Web.Exhentai.Errors import qualified Web.Exhentai.Parsing.Gallery as G-import Web.Exhentai.Types-import Web.Exhentai.Types.CookieT import Web.Exhentai.Utils import Prelude hiding (length) import qualified Prelude as P +data Gallery = Gallery+ { galleryId :: {-# UNPACK #-} !Int,+ token :: {-# UNPACK #-} !Text+ }+ deriving (Show, Eq)++toGalleryLink :: Gallery -> Text+toGalleryLink Gallery {..} = "https://exhentai.org/g/" <> pack (show galleryId) <> "/" <> token <> "/"++parseGalleryLink :: Text -> Maybe Gallery+parseGalleryLink = parseMaybe galleryLink+ where+ galleryLink :: Parser Gallery+ galleryLink = do+ _ <- chunk "https://exhentai.org/g/"+ galleryId <- decimal+ _ <- single '/'+ token <- takeWhile1P Nothing (/= '/')+ _ <- optional $ single '/'+ pure Gallery {..}++data TagCategory+ = Language+ | Parody+ | Character+ | Group+ | Artist+ | Male+ | Female+ | Misc'+ | Reclass+ deriving (Show, Eq, Enum)++readTagCat :: Text -> Maybe TagCategory+readTagCat "language:" = Just Language+readTagCat "parody:" = Just Parody+readTagCat "character:" = Just Character+readTagCat "group:" = Just Group+readTagCat "artist:" = Just Artist+readTagCat "male:" = Just Male+readTagCat "female:" = Just Female+readTagCat "misc:" = Just Misc'+readTagCat "reclass:" = Just Reclass+readTagCat _ = Nothing++data GalleryCategory+ = Misc+ | Doujinshi+ | Manga+ | ArtistCG+ | GameCG+ | ImageSet+ | Cosplay+ | AsianPorn+ | NonH+ | Western+ | Private+ deriving (Show, Eq, Ord, Enum, Bounded)++allGalleryCats :: Set GalleryCategory+allGalleryCats = fromList [Misc .. Private]++readCat :: Text -> Maybe GalleryCategory+readCat "Doujinshi" = Just Doujinshi+readCat "Manga" = Just Manga+readCat "Artist CG" = Just ArtistCG+readCat "Game CG" = Just GameCG+readCat "Non-H" = Just NonH+readCat "Image Set" = Just ImageSet+readCat "Western" = Just Western+readCat "Cosplay" = Just Cosplay+readCat "Misc" = Just Misc+readCat "Private" = Just Private+readCat "Asian Porn" = Just AsianPorn+readCat _ = Nothing+ -- | Information about a gallery data GalleryInfo = GalleryInfo { title :: {-# UNPACK #-} Text, previewLink :: {-# UNPACK #-} Text,- category :: GalleryCat,+ category :: GalleryCategory, jaTitle :: Maybe Text, uploader :: {-# UNPACK #-} Text,- rating :: {-# UNPACK #-} Float,+ rating :: {-# UNPACK #-} Double, ratingCount :: {-# UNPACK #-} Int, favoriteCount :: {-# UNPACK #-} Int,- tags :: [(G.TagCategory, [Text])],+ tags :: [(TagCategory, [Text])], uploadTime :: {-# UNPACK #-} UTCTime, newer :: Maybe Gallery, parent :: Maybe Gallery,@@ -45,14 +137,14 @@ archiverLink :: {-# UNPACK #-} Text, torrentLink :: {-# UNPACK #-} Text }- deriving (Show, Eq, Generic)+ deriving (Show, Eq) data Visibility = Visible | Replaced | Expunged | Unknown {-# UNPACK #-} Text- deriving (Show, Eq, Generic)+ deriving (Show, Eq) readVisibility :: Text -> Visibility readVisibility "Yes" = Visible@@ -66,33 +158,111 @@ title <- annotate "title" $ d ^?: G.enTitle previewLink <- annotate "preview link" $ d ^?: G.previewStr >>= parsePreviewLink let jaTitle = d ^?: G.jaTitle- category <- annotate "category" $ d ^?: G.category+ category <- annotate "category" $ d ^?: G.category >>= readCat uploader <- annotate "uploader" $ d ^?: G.uploader- (coerce -> rating) <- annotate "average rating" $ d ^?: G.averageRating+ rating <- annotate "average rating" $ d ^?: G.averageRating >>= parseAverageRating ratingCount <- annotate "rating count" $ d ^?: G.ratingCount- (coerce -> archiverLink) <- annotate "archiver link" $ d ^?: G.popupLink- (coerce -> torrentLink) <- annotate "torrent link" $ case d ^..: G.popupLink of (_ : tl : _) -> Just tl; _ -> Nothing- let newer = d ^?: G.newer+ archiverLink <- annotate "archiver link" $ d ^?: G.popupLink >>= parsePopUpLink+ torrentLink <- annotate "torrent link" $ case d ^..: G.popupLink of (_ : tl : _) -> Just tl; _ -> Nothing+ let newer = d ^?: G.newer >>= parseGalleryLink case d ^..: G.metaValues of (time : parn : vis : lang : _ : len : fav : _) -> do- uploadTime <- annotate "upload time" $ time ^? lower . _Content >>= parseUploadTime- let parent = parn ^? lower . _Element . attr "href" . _GalleryLink- (readVisibility -> visibility) <- annotate "visibility" $ vis ^? lower . _Content- (strip -> language) <- annotate "language" $ lang ^? lower . _Content- (coerce -> length) <- annotate "length" $ len ^? lower . _Content >>= parseGalleryLength- let cats = d ^..: G.tagCategory+ uploadTime <- annotate "upload time" $ time ^? pre lower % _Content >>= parseUploadTime+ let parent = parn ^? pre lower % _Element % attr "href" >>= parseGalleryLink+ (readVisibility -> visibility) <- annotate "visibility" $ vis ^? pre lower % _Content+ (strip -> language) <- annotate "language" $ lang ^? pre lower % _Content+ length <- annotate "length" $ len ^? pre lower % _Content >>= parseGalleryLength+ let cats = mapMaybe readTagCat (d ^..: G.tagCategory) let tags' = map (^.. G.tags) $ d ^..: G.tagsByCategory- unless (P.length cats == P.length tags') $ Left ""+ unless (P.length cats == P.length tags') $ Left "length of categories and tags does not match" let tags = zip cats tags'- (coerce -> favoriteCount) <- annotate "favorite count" $ fav ^? lower . _Content >>= parseFavoriteCount+ favoriteCount <- annotate "favorite count" $ fav ^? pre lower % _Content >>= parseFavoriteCount pure GalleryInfo {..} _ -> Left "extracting metadata" -- | Fetch a gallery's 'GalleryInfo'-fetchGalleryInfo :: MonadHttpState m => Gallery -> m GalleryInfo+fetchGalleryInfo ::+ Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>+ Gallery ->+ m GalleryInfo fetchGalleryInfo g = do let url = toGalleryLink g d <- htmlRequest' url case parseGallery d of- Left err -> throwM $ XMLParseFailure err url+ Left err -> throw $ XMLParseFailure err url Right info -> pure info+{-# INLINEABLE fetchGalleryInfo #-}++--------------------------------------------------+--++type Parser = Parsec Void Text++parsePopUpLink :: Text -> Maybe Text+parsePopUpLink = parseMaybe archiverLink+ where+ archiverLink :: Parser Text+ archiverLink = do+ _ <- chunk "return popUp('"+ url <- takeWhile1P Nothing (/= '\'')+ _ <- takeRest+ pure url++parseAverageRating :: Text -> Maybe Double+parseAverageRating = parseMaybe averageRating+ where+ averageRating :: Parser Double+ averageRating =+ ( do+ _ <- chunk "Average: "+ float+ )+ <|> (chunk "Not Yet Rated" >> pure 0)++parseGalleryLength :: Text -> Maybe Int+parseGalleryLength = parseMaybe galleryLength+ where+ galleryLength :: Parser Int+ galleryLength = do+ d <- decimal+ _ <- chunk " pages"+ pure d++parseFavoriteCount :: Text -> Maybe Int+parseFavoriteCount = parseMaybe favoriteCount+ where+ once = do+ _ <- chunk "Once"+ pure 1+ never = do+ _ <- chunk "Never"+ pure 0+ favoriteCount :: Parser Int+ favoriteCount =+ ( do+ d <- decimal+ _ <- chunk " times"+ pure d+ )+ <|> once+ <|> never++parsePreviewLink :: Text -> Maybe Text+parsePreviewLink = parseMaybe previewLink+ where+ previewLink :: Parser Text+ previewLink = do+ _ <- many $ do+ notFollowedBy urlOpening+ anySingle+ _ <- urlOpening+ url <- takeWhile1P Nothing (/= ')')+ _ <- takeRest+ pure url+ urlOpening :: Parser Text+ urlOpening = chunk "url("++makeFieldLabelsWith noPrefixFieldLabels ''GalleryInfo+makePrismLabels ''TagCategory+makePrismLabels ''GalleryCategory+makePrismLabels ''Visibility
src/Web/Exhentai/API/MPV.hs view
@@ -1,42 +1,47 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE UndecidableInstances #-} +-- | MPV (multi-page viewer) API. module Web.Exhentai.API.MPV ( DispatchRequest (..),- DispatchResult (..),- Vars (..), Server (..), Dim (..),- fetchMpv,- toRequests,- imageDispatch,+ buildRequest, fetchImage,- fetchImage', ) where import Conduit import Control.Applicative-import Control.Lens ((^.))-import Control.Monad.Catch+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, unpack)-import GHC.Generics-import Network.HTTP.Client.Conduit+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.Parsing.MPV-import Web.Exhentai.Types-import Web.Exhentai.Types.CookieT import Web.Exhentai.Utils+import Prelude hiding ((!!)) data Server- = HAtH Int- | Other Text- deriving (Show, Eq, Generic)+ = HAtH {-# UNPACK #-} Int+ | Other {-# UNPACK #-} Text+ deriving (Show, Eq) instance FromJSON Server where parseJSON v =@@ -57,21 +62,21 @@ data DispatchResult = DispatchResult { -- | A piece of text describing the dimensions and the size of this image- dimension :: Text,+ dimension :: {-# UNPACK #-} Text, -- | The path part of the url pointing to the original image- origImgPath :: Text,+ origImgPath :: {-# UNPACK #-} Text, -- | The path part of the url that searches for the gallery containing this image- searchPath :: Text,+ searchPath :: {-# UNPACK #-} Text, -- | The path part of the non-mpv page that displays this image- galleryPath :: Text,- width :: Dim,- height :: Dim,+ galleryPath :: {-# UNPACK #-} Text,+ width :: {-# UNPACK #-} Dim,+ height :: {-# UNPACK #-} Dim, -- | The full url to this image- imgLink :: Text,+ imgLink :: {-# UNPACK #-} Text, -- | The server that serves this image server :: Server }- deriving (Show, Eq, Generic)+ deriving (Show, Eq) instance FromJSON DispatchResult where parseJSON = withObject "imagedispatch result" $ \o ->@@ -86,13 +91,13 @@ <*> o .: "s" data DispatchRequest = DispatchRequest- { galleryId :: Int,- page :: Int,- imgKey :: Text,- mpvKey :: Text,+ { galleryId :: {-# UNPACK #-} Int,+ page :: {-# UNPACK #-} Int,+ imgKey :: {-# UNPACK #-} Text,+ mpvKey :: {-# UNPACK #-} Text, exclude :: Maybe Server }- deriving (Show, Eq, Generic)+ deriving (Show, Eq) instance ToJSON DispatchRequest where toJSON DispatchRequest {..} = object l@@ -120,34 +125,64 @@ 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 :: (MonadHttpState m, MonadIO m) => Gallery -> m Vars+fetchMpv ::+ Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket, Throw ExhentaiError] m =>+ Gallery ->+ m Vars fetchMpv g = htmlRequest' (toMpvLink g) >>= parseMpv+{-# INLINEABLE fetchMpv #-} -parseMpv :: (MonadIO m, MonadThrow m) => Document -> m Vars+parseMpv :: Effs '[Throw ExhentaiError] m => Document -> m Vars parseMpv doc = do- let script = doc ^. allScripts- res <- liftIO $ extractEnv script- case res of- Error e -> throwM $ ExtractionFailure e- Success vars -> pure vars+ 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 :: MonadHttpState m => DispatchRequest -> m DispatchResult+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 -> throwM $ JSONParseFailure e+ Left e -> throw $ JSONParseFailure e Right res -> pure res+{-# INLINEABLE imageDispatch #-} -- | Fetch an image with a 'DispatchRequest'-fetchImage :: (MonadHttpState m, MonadIO n) => DispatchRequest -> ContT r m (Response (ConduitT i ByteString n ()))+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' :: (MonadHttpState m, MonadIO n) => DispatchRequest -> m (Response (ConduitT i ByteString n ()))+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@@ -157,3 +192,12 @@ 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
src/Web/Exhentai/API/Search.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE StrictData #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE UndecidableInstances #-} module Web.Exhentai.API.Search ( SearchQuery (..),@@ -6,36 +8,40 @@ search, searchRecur, searchRecurResumable,- searchRecurResumable', fetchSearchPage,- fetchSearchPage', ) where import Conduit-import Control.Lens ((...))-import Control.Lens.Fold+import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Exh import Control.Monad-import Data.Set (Set, (\\))+import Data.Maybe+import Data.Set (Set, toList, (\\)) 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 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.Types-import Web.Exhentai.Types.CookieT import Web.Exhentai.Utils import Prelude hiding (last) +toBitField :: Set GalleryCategory -> Int+toBitField = sum . map ((2 ^) . fromEnum) . toList+ data SearchQuery = SearchQuery- { categories :: Maybe (Set GalleryCat),+ { categories :: Maybe (Set GalleryCategory), searchString :: {-# UNPACK #-} Text }- deriving (Show, Eq, Generic)+ deriving (Show, Eq) -queryArgCat :: Set GalleryCat -> Int+queryArgCat :: Set GalleryCategory -> Int queryArgCat s = toBitField $ allGalleryCats \\ s data SearchResult = SearchResult@@ -43,33 +49,44 @@ prevPage :: Maybe Text, nextPage :: Maybe Text }- deriving (Show, Eq, Generic)+ deriving (Show, Eq) parseSearchPage :: Document -> SearchResult parseSearchPage d =- let galleries = d ^..: galleryPreviewElement . galleryLink- fld :: Fold Document Element- fld = body ... pagesElem+ let galleries = mapMaybe parseGalleryLink $ d ^..: galleryPreviewElement % galleryLink+ fld :: Traversal' Document Element+ fld = body .// pagesElem prevPage = do- first <- firstOf fld d- first ^? linkOf+ first <- headOf fld d+ first ^? pre linkOf nextPage = do last <- lastOf fld d- last ^? linkOf+ last ^? pre linkOf in SearchResult {..} -- | Fetch a search page using a 'Request'-fetchSearchPage' :: MonadHttpState m => Request -> m SearchResult+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 :: MonadHttpState m => Text -> m SearchResult+fetchSearchPage ::+ Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m =>+ Text ->+ m SearchResult fetchSearchPage = fetchSearchPage' <=< formRequest . unpack+{-# INLINEABLE fetchSearchPage #-} -- | Search a search query-search :: MonadHttpState m => SearchQuery -> m SearchResult+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"@@ -81,18 +98,23 @@ ) initReq fetchSearchPage' req+{-# INLINEABLE search #-} -- | Iterate through all the Galleries asosciated with a search query, putting them into a stream-searchRecur :: MonadHttpState m => SearchQuery -> ConduitT i Gallery m ()+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' ::- MonadHttpState m =>+ Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => -- | url Text -> ConduitT i Gallery m ()@@ -102,18 +124,23 @@ case nextPage of Nothing -> pure () Just url' -> searchRecur' url'+{-# INLINEABLE searchRecur' #-} -- | A resumable version of 'searchRecur' that reports it's progress.-searchRecurResumable :: MonadHttpState m => SearchQuery -> ConduitT i (Either Text Gallery) m ()+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' ::- MonadHttpState m =>+ Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => -- | url Text -> ConduitT i (Either Text Gallery) m ()@@ -124,3 +151,7 @@ case nextPage of Nothing -> pure () Just url' -> searchRecurResumable' url'+{-# INLINEABLE searchRecurResumable' #-}++makeFieldLabelsWith noPrefixFieldLabels ''SearchQuery+makeFieldLabelsWith noPrefixFieldLabels ''SearchResult
src/Web/Exhentai/API/Watched.hs view
@@ -4,19 +4,27 @@ ) where +import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Exh+import Data.Maybe+import Network.HTTP.Client hiding (Cookie)+import Optics.Core+import Web.Exhentai.API.Gallery import Web.Exhentai.Parsing.Search-import Web.Exhentai.Types-import Web.Exhentai.Types.CookieT import Web.Exhentai.Utils -- | Fetch the list of watched galleries-fetchWatched :: MonadHttpState m => m [Gallery]+fetchWatched :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket] m => m [Gallery] fetchWatched = do d <- htmlRequest' "https://exhentai.org/watched"- pure $ d ^..: galleryPreviewElement . galleryLink+ pure $ mapMaybe parseGalleryLink $ d ^..: galleryPreviewElement % galleryLink+{-# INLINEABLE fetchWatched #-} -- | Fetch the list of popular galleries-fetchPopular :: MonadHttpState m => m [Gallery]+fetchPopular :: Effs '[Http, Error HttpException, Cookie, ConduitIO, Bracket] m => m [Gallery] fetchPopular = do d <- htmlRequest' "https://exhentai.org/popular"- pure $ d ^..: galleryPreviewElement . galleryLink+ pure $ mapMaybe parseGalleryLink $ d ^..: galleryPreviewElement % galleryLink+{-# INLINEABLE fetchPopular #-}
src/Web/Exhentai/Errors.hs view
@@ -6,7 +6,6 @@ import Control.Exception import Data.Text (Text)-import GHC.Generics data ExhentaiError = JSONParseFailure String@@ -14,6 +13,6 @@ { reason :: Text, url :: Text }- | ExtractionFailure String- deriving (Show, Eq, Generic)+ | ExtractionFailure+ deriving (Show, Eq) deriving (Exception)
src/Web/Exhentai/Parsing/Gallery.hs view
@@ -1,122 +1,109 @@ {-# LANGUAGE RankNTypes #-} +-- | Internal module module Web.Exhentai.Parsing.Gallery where -import Control.Lens-import Data.Text (Text, pack)-import GHC.Generics+import Data.Text (Text)+import Optics.Core import Text.XML hiding (readFile)-import Text.XML.Lens-import Web.Exhentai.Types- ( AverageRating,- Gallery,- GalleryCat,- PopUpLink,- _AverageRating,- _GalleryCat,- _GalleryLink,- _PopUpLink,- )+import Text.XML.Optics import Web.Exhentai.Utils import Prelude hiding (div, id, readFile) -data TagCategory- = Language- | Parody- | Character- | Group- | Artist- | Male- | Female- | Misc- | Reclass- deriving (Show, Eq, Enum, Generic)--readTagCat :: Text -> Maybe TagCategory-readTagCat "language:" = Just Language-readTagCat "parody:" = Just Parody-readTagCat "character:" = Just Character-readTagCat "group:" = Just Group-readTagCat "artist:" = Just Artist-readTagCat "male:" = Just Male-readTagCat "female:" = Just Female-readTagCat "misc:" = Just Misc-readTagCat "reclass:" = Just Reclass-readTagCat _ = Nothing--_TagCategory :: Prism' Text TagCategory-_TagCategory = prism' (pack . show) readTagCat--meta :: Traversal' Element Element-meta = div . cl "gm"+meta :: AffineTraversal' Element Element+meta = div % cl "gm"+{-# INLINE meta #-} title :: Traversal' Element Element-title = meta ... div . id "gd2"+title = meta .// (div % id "gd2")+{-# INLINE title #-} enTitle :: Traversal' Element Text-enTitle = title ... h1 . id "gn" . lower . _Content+enTitle = title .// (h1 % id "gn" <% lower % _Content)+{-# INLINE enTitle #-} jaTitle :: Traversal' Element Text-jaTitle = title ... h1 . id "gj" . lower . _Content+jaTitle = title .// (h1 % id "gj" <% lower % _Content)+{-# INLINE jaTitle #-} mainMeta :: Traversal' Element Element-mainMeta = meta ... div . id "gmid"+mainMeta = meta .// (div % id "gmid")+{-# INLINE mainMeta #-} mainMetaL :: Traversal' Element Element-mainMetaL = mainMeta ... div . id "gd3"+mainMetaL = mainMeta .// (div % id "gd3")+{-# INLINE mainMetaL #-} mainMetaM :: Traversal' Element Element-mainMetaM = mainMeta ... div . id "gd4"+mainMetaM = mainMeta .// (div % id "gd4")+{-# INLINE mainMetaM #-} mainMetaR :: Traversal' Element Element-mainMetaR = mainMeta ... div . id "gd5"+mainMetaR = mainMeta .// (div % id "gd5")+{-# INLINE mainMetaR #-} previewStr :: Traversal' Element Text-previewStr = meta ... id "gleft" ... id "gd1" ... attr "style"+previewStr = meta .// id "gleft" .// id "gd1" .// attr "style"+{-# INLINE previewStr #-} -category :: Traversal' Element GalleryCat-category = mainMetaL ... div . id "gdc" ... div . lower . _Content . _GalleryCat+category :: Traversal' Element Text+category = mainMetaL .// (div % id "gdc") .// (div <% lower % _Content)+{-# INLINE category #-} uploader :: Traversal' Element Text-uploader = mainMetaL ... div . id "gdn" ... a . lower . _Content+uploader = mainMetaL .// (div % id "gdn") .// (a % lower %> _Content)+{-# INLINE uploader #-} metaPath :: Traversal' Element Element-metaPath = mainMetaL ... div . id "gdd" ... table ... tr+metaPath = mainMetaL .// (div % id "gdd") .// table .// tr+{-# INLINE metaPath #-} metaKeys :: Traversal' Element Text-metaKeys = metaPath ... cl "gdt1" . lower . _Content+metaKeys = metaPath .// (cl "gdt1" <% lower % _Content)+{-# INLINE metaKeys #-} metaValues :: Traversal' Element Element-metaValues = metaPath ... cl "gdt2"+metaValues = metaPath .// cl "gdt2"+{-# INLINE metaValues #-} -parent :: Traversal' Element Gallery-parent = lower . _Element . attr "href" . _GalleryLink+parent :: Traversal' Element Text+parent = lower %> _Element % attr "href"+{-# INLINE parent #-} ratingCount :: Traversal' Element Int ratingCount =- mainMetaL ... id "gdr" ... table ... tr ... id "grt3" ... lower . _Content . viaShowRead+ mainMetaL .// id "gdr" .// table .// tr .// id "grt3" .// (lower %> _Content % viaShowRead)+{-# INLINE ratingCount #-} -averageRating :: Traversal' Element AverageRating+averageRating :: Traversal' Element Text averageRating =- mainMetaL ... id "gdr" ... table ... tr ... id "rating_label" . lower . _Content . _AverageRating+ mainMetaL .// id "gdr" .// table .// tr .// (id "rating_label" % lower %> _Content)+{-# INLINE averageRating #-} tagList :: Traversal' Element Element-tagList = mainMetaM ... div . id "taglist" ... table ... tr+tagList = mainMetaM .// (div % id "taglist") .// table .// tr+{-# INLINE tagList #-} -tagCategory :: Traversal' Element TagCategory-tagCategory = tagList ... td . cl "tc" . lower . _Content . _TagCategory+tagCategory :: Traversal' Element Text+tagCategory = tagList .// (td % cl "tc" % lower %> _Content)+{-# INLINE tagCategory #-} tagsByCategory :: Traversal' Element Element-tagsByCategory = tagList ... td . withoutAttribute "class"+tagsByCategory = tagList .// (td % withoutAttribute "class")+{-# INLINE tagsByCategory #-} tags :: Traversal' Element Text-tags = td ... div ... a . lower . _Content+tags = td .// div .// (a % lower %> _Content)+{-# INLINE tags #-} -popupLink :: Traversal' Element PopUpLink-popupLink = mainMetaR ... named "p" ... attr "onclick" . _PopUpLink+popupLink :: Traversal' Element Text+popupLink = mainMetaR .// named "p" .// attr "onclick"+{-# INLINE popupLink #-} imagePages :: Traversal' Element Text-imagePages = div . id "gdt" ... cl "gdtl" ... a . attr "href"+imagePages = (div % id "gdt") .// cl "gdtl" .// (a % attr "href")+{-# INLINE imagePages #-} -newer :: Traversal' Element Gallery-newer = div . id "gnd" ... a . attr "href" . _GalleryLink+newer :: Traversal' Element Text+newer = (div % id "gnd") .// (a % attr "href")+{-# INLINE newer #-}
src/Web/Exhentai/Parsing/Image.hs view
@@ -1,13 +1,16 @@+-- | Internal module module Web.Exhentai.Parsing.Image where -import Control.Lens import Data.Text (Text)-import Text.XML.Lens+import Optics.Core+import Text.XML.Optics import Web.Exhentai.Utils import Prelude hiding (id) imageSrc :: Traversal' Element Text-imageSrc = id "i1" ... id "i3" ... a ... img . attr "src"+imageSrc = id "i1" .// id "i3" .// a .// (img % attr "src")+{-# INLINE imageSrc #-} -nextPage :: Traversal' Element Text-nextPage = id "i1" ... id "i3" ... a . attr "href"+nextImage :: Traversal' Element Text+nextImage = id "i1" .// id "i3" .// (a % attr "href")+{-# INLINE nextImage #-}
− src/Web/Exhentai/Parsing/MPV.hs
@@ -1,55 +0,0 @@-{-# LANGUAGE StrictData #-}--module Web.Exhentai.Parsing.MPV where--import Control.Lens-import Data.Aeson-import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8)-import GHC.Generics-import Quickjs-import Text.XML.Lens-import Web.Exhentai.Utils--allScripts :: Traversal' Document Text-allScripts = body ... scripts . lower . _Content--data MpvImage = MpvImage- { name :: {-# UNPACK #-} Text,- key :: {-# UNPACK #-} Text,- thumbnail :: {-# UNPACK #-} Text- }- deriving (Show, Eq, Generic)--instance FromJSON MpvImage where- parseJSON = withObject "mpv image" $ \o ->- MpvImage- <$> o .: "n"- <*> o .: "k"- <*> o .: "t"---- | All the variables defined in the scripts that came with the MPV-data Vars = Vars- { gid :: {-# UNPACK #-} Int,- mpvkey :: {-# UNPACK #-} Text,- apiUrl :: {-# UNPACK #-} Text,- pageCount :: {-# UNPACK #-} Int,- imageList :: [MpvImage]- }- deriving (Show, Eq, Generic)--extractEnv :: Text -> IO (Result Vars)-extractEnv script = quickjs $ do- eval_ $ encodeUtf8 script- gid' <- eval "gid"- mpvkey' <- eval "mpvkey"- imageList' <- eval "imagelist"- apiUrl' <- eval "api_url"- pageCount' <- eval "pagecount"- pure $ do- gid <- fromJSON gid'- mpvkey <- fromJSON mpvkey'- imageList <- fromJSON imageList'- apiUrl <- fromJSON apiUrl'- pageCount <- fromJSON pageCount'- pure Vars {..}
src/Web/Exhentai/Parsing/Search.hs view
@@ -1,32 +1,36 @@+-- | Internal modules module Web.Exhentai.Parsing.Search where -import Control.Lens import Data.Text (Text)-import Text.XML.Lens-import Web.Exhentai.Types+import Optics.Core+import Text.XML.Optics import Web.Exhentai.Utils import Prelude hiding (div) pages :: Traversal' Element Int-pages = pagesElem ... a . lower . _Content . viaShowRead+pages = pagesElem .// (a % lower %> _Content % viaShowRead)+{-# INLINE pages #-} pagesElem :: Traversal' Element Element-pagesElem = cl "ido" ... div ... cl "ptt" ... tr ... td+pagesElem = cl "ido" .// div .// cl "ptt" .// tr .// td+{-# INLINE pagesElem #-} linkOf :: Traversal' Element Text-linkOf = lower . _Element . attr "href"+linkOf = lower %> _Element % attr "href"+{-# INLINE linkOf #-} galleryPreviewElement :: Traversal' Element Element-galleryPreviewElement = cl "ido" ... div ... cl "itg glte" ... tr+galleryPreviewElement = cl "ido" .// div .// cl "itg glte" .// tr+{-# INLINE galleryPreviewElement #-} previewImage :: Traversal' Element Text-previewImage = tr ... cl "gl1e" ... div ... a ... img . attr "src"+previewImage = tr .// cl "gl1e" .// div .// a .// (img % attr "src")+{-# INLINE previewImage #-} title :: Traversal' Element Text-title = tr ... cl "gl1e" ... div ... a ... img . attr "title"--galleryLink :: Traversal' Element Gallery-galleryLink = tr ... cl "gl1e" ... div ... a . attr "href" . _GalleryLink+title = tr .// cl "gl1e" .// div .// a .// (img % attr "title")+{-# INLINE title #-} -galleryLength :: Traversal' Element GalleryLength-galleryLength = tr ... cl "gl2e" ... div ... cl "gl3e" ... lower . _Content . _GalleryLength+galleryLink :: Traversal' Element Text+galleryLink = tr .// cl "gl1e" .// div .// (a % attr "href")+{-# INLINE galleryLink #-}
− src/Web/Exhentai/Types.hs
@@ -1,187 +0,0 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}--module Web.Exhentai.Types where--import Control.Applicative ((<|>))-import Control.Lens-import Data.Set (Set, fromList, toList)-import Data.Text (Text, pack)-import Data.Void-import GHC.Generics-import Text.Megaparsec- ( MonadParsec (notFollowedBy, takeWhile1P),- Parsec,- anySingle,- chunk,- many,- optional,- parseMaybe,- single,- takeRest,- )-import Text.Megaparsec.Char.Lexer--type Parser = Parsec Void Text--data GalleryCat- = Misc- | Doujinshi- | Manga- | ArtistCG- | GameCG- | ImageSet- | Cosplay- | AsianPorn- | NonH- | Western- | Private- deriving (Show, Eq, Ord, Enum, Bounded)--allGalleryCats :: Set GalleryCat-allGalleryCats = fromList [Misc .. Private]--toBitField :: Set GalleryCat -> Int-toBitField = sum . map ((2 ^) . fromEnum) . toList--showCat :: GalleryCat -> Text-showCat Doujinshi = "Doujinshi"-showCat Manga = "Manga"-showCat ArtistCG = "Artist CG"-showCat GameCG = "Game CG"-showCat NonH = "Non-H"-showCat ImageSet = "Image Set"-showCat Western = "Western"-showCat Cosplay = "Cosplay"-showCat Misc = "Misc"-showCat Private = "Private"-showCat AsianPorn = "Asian Porn"--readCat :: Text -> Maybe GalleryCat-readCat "Doujinshi" = Just Doujinshi-readCat "Manga" = Just Manga-readCat "Artist CG" = Just ArtistCG-readCat "Game CG" = Just GameCG-readCat "Non-H" = Just NonH-readCat "Image Set" = Just ImageSet-readCat "Western" = Just Western-readCat "Cosplay" = Just Cosplay-readCat "Misc" = Just Misc-readCat "Private" = Just Private-readCat "Asian Porn" = Just AsianPorn-readCat _ = Nothing--_GalleryCat :: Prism' Text GalleryCat-_GalleryCat = prism' showCat readCat--newtype PopUpLink = PopUpLink {unLink :: Text}- deriving newtype (Show, Eq)--_PopUpLink :: Prism' Text PopUpLink-_PopUpLink = prism' unLink parsePopUpLink--parsePopUpLink :: Text -> Maybe PopUpLink-parsePopUpLink = parseMaybe archiverLink- where- archiverLink :: Parser PopUpLink- archiverLink = do- _ <- chunk "return popUp('"- url <- takeWhile1P Nothing (/= '\'')- _ <- takeRest- pure $ PopUpLink url--newtype AverageRating = AverageRating {unRating :: Float}- deriving newtype (Show, Eq)--_AverageRating :: Prism' Text AverageRating-_AverageRating = prism' (pack . show . unRating) parseAverageRating--parseAverageRating :: Text -> Maybe AverageRating-parseAverageRating = parseMaybe averageRating- where- averageRating :: Parser AverageRating- averageRating =- ( do- _ <- chunk "Average: "- AverageRating <$> float- )- <|> (chunk "Not Yet Rated" >> pure (AverageRating 0))--newtype GalleryLength = GalleryLength {unGalleryLength :: Int}- deriving newtype (Show, Eq)--_GalleryLength :: Prism' Text GalleryLength-_GalleryLength = prism' (pack . show . unGalleryLength) parseGalleryLength--parseGalleryLength :: Text -> Maybe GalleryLength-parseGalleryLength = parseMaybe galleryLength- where- galleryLength :: Parser GalleryLength- galleryLength = do- d <- decimal- _ <- chunk " pages"- pure $ GalleryLength d--newtype FavoriteCount = FavoriteCount {unFavoriteCount :: Int}- deriving newtype (Show, Eq)--parseFavoriteCount :: Text -> Maybe FavoriteCount-parseFavoriteCount = parseMaybe favoriteCount- where- once = do- _ <- chunk "Once"- pure $ FavoriteCount 1- never = do- _ <- chunk "Never"- pure $ FavoriteCount 0- favoriteCount :: Parser FavoriteCount- favoriteCount =- ( do- d <- decimal- _ <- chunk " times"- pure $ FavoriteCount d- )- <|> once- <|> never--data Gallery = Gallery- { galleryId :: {-# UNPACK #-} !Int,- token :: {-# UNPACK #-} !Text- }- deriving (Show, Eq, Generic)--_GalleryLink :: Prism' Text Gallery-_GalleryLink = prism' toGalleryLink parseGalleryLink--toGalleryLink :: Gallery -> Text-toGalleryLink Gallery {..} = "https://exhentai.org/g/" <> pack (show galleryId) <> "/" <> token <> "/"--toMpvLink :: Gallery -> Text-toMpvLink Gallery {..} = "https://exhentai.org/mpv/" <> pack (show galleryId) <> "/" <> token <> "/"--parseGalleryLink :: Text -> Maybe Gallery-parseGalleryLink = parseMaybe galleryLink- where- galleryLink :: Parser Gallery- galleryLink = do- _ <- chunk "https://exhentai.org/g/"- galleryId <- decimal- _ <- single '/'- token <- takeWhile1P Nothing (/= '/')- _ <- optional $ single '/'- pure Gallery {..}--parsePreviewLink :: Text -> Maybe Text-parsePreviewLink = parseMaybe previewLink- where- previewLink :: Parser Text- previewLink = do- _ <- many $ do- notFollowedBy urlOpening- anySingle- _ <- urlOpening- url <- takeWhile1P Nothing (/= ')')- _ <- takeRest- pure url- urlOpening :: Parser Text- urlOpening = chunk "url("
− src/Web/Exhentai/Types/CookieT.hs
@@ -1,180 +0,0 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE UndecidableInstances #-}--module Web.Exhentai.Types.CookieT where--import Conduit-import Control.Concurrent-import Control.Monad.Base-import Control.Monad.Catch-import Control.Monad.Except-import Control.Monad.Reader-import Control.Monad.Time-import Control.Monad.Trans.Control-import Control.Retry-import Data.ByteString (ByteString)-import Data.Function ((&))-import GHC.Generics-import Network.HTTP.Client.Conduit-import Network.HTTP.Client.MultipartFormData-import Network.HTTP.Client.TLS--newtype Policy = Policy RetryPolicy--class (Monad m, MonadCatch m, MonadThrow m) => MonadHttp m where- getRetryPolicy :: m Policy- formRequest :: String -> m Request- attachFormData :: [Part] -> Request -> m Request- respOpen :: MonadIO n => Request -> m (Response (ConduitT i ByteString n ()))- respClose :: Response body -> m ()- reqNoBody :: Request -> m (Response ())--class (MonadMask m, MonadTime m, MonadHttp m, MonadIO m) => MonadHttpState m where- takeCookieJar :: m CookieJar- readCookieJar :: m CookieJar- putCookieJar :: CookieJar -> m ()--modifyingJar :: MonadHttpState m => Request -> m ()-modifyingJar req =- bracketOnError- takeCookieJar- putCookieJar- $ \jar -> do- let req' = req {cookieJar = Just jar}- resp <- retryWhenTimeout $ reqNoBody req'- putCookieJar $ responseCookieJar resp- pure ()--openWithJar :: (MonadHttpState m, MonadIO n) => Request -> m (Response (ConduitT i ByteString n ()))-openWithJar req = do- jar <- readCookieJar- let req' = req {cookieJar = Just jar}- respOpen req'--withJar :: (MonadHttpState m, MonadIO n) => Request -> (ConduitT i ByteString n () -> m a) -> m a-withJar req k = do- jar <- readCookieJar- let req' = req {cookieJar = Just jar}- bracket- (respOpen req')- respClose- (k . responseBody)--data CookieEnv = CookieEnv- { policy :: Policy,- jarRef :: MVar CookieJar,- manager :: {-# UNPACK #-} !Manager- }- deriving (Generic)--instance HasHttpManager CookieEnv where- getHttpManager = manager--newtype CookieT m a = CookieT {unCookieT :: ReaderT CookieEnv m a}- deriving newtype- ( Functor,- Applicative,- Monad,- MonadReader CookieEnv,- MonadThrow,- MonadCatch,- MonadMask,- MonadIO,- MonadResource,- MonadUnliftIO,- MonadError e,- MonadBase b,- MonadBaseControl b,- MonadTime- )--runCookieT :: MonadIO m => RetryPolicy -> CookieT m a -> m a-runCookieT (Policy -> policy) m = do- manager <- liftIO newTlsManager- jarRef <- liftIO $ newMVar mempty- m- & unCookieT- & flip runReaderT CookieEnv {..}--instance MonadTrans CookieT where- lift = CookieT . lift--instance- ( MonadIO m,- MonadUnliftIO m,- MonadCatch m,- MonadThrow m- ) =>- MonadHttp (CookieT m)- where- getRetryPolicy = asks policy- formRequest = parseRequest- attachFormData = formDataBody- respOpen = responseOpen- respClose = responseClose- reqNoBody = httpNoBody--instance- {-# OVERLAPPABLE #-}- ( MonadHttp m,- MonadTrans f,- Monad (f m),- MonadCatch (f m),- MonadThrow (f m)- ) =>- MonadHttp (f m)- where- getRetryPolicy = lift getRetryPolicy- formRequest = lift . formRequest- attachFormData p = lift . attachFormData p- respOpen = lift . respOpen- respClose = lift . respClose- reqNoBody = lift . reqNoBody--retryWhenTimeout :: MonadHttpState m => m a -> m a-retryWhenTimeout action = do- Policy policy <- getRetryPolicy- recovering policy handlers (const action)- where- handlers =- skipAsyncExceptions- ++ [ const (Handler (pure . judge))- ]- judge (HttpExceptionRequest _ c)- | ResponseTimeout <- c = True- | ConnectionTimeout <- c = True- judge _ = False--instance- ( MonadMask m,- MonadUnliftIO m,- MonadTime m- ) =>- MonadHttpState (CookieT m)- where- takeCookieJar = do- ref <- asks jarRef- liftIO $ takeMVar ref- putCookieJar jar = do- ref <- asks jarRef- liftIO $ putMVar ref jar- readCookieJar = do- ref <- asks jarRef- liftIO $ readMVar ref--instance- {-# OVERLAPPABLE #-}- ( MonadHttpState m,- MonadTrans f,- MonadMask (f m),- MonadTime (f m),- MonadHttp (f m),- MonadIO (f m)- ) =>- MonadHttpState (f m)- where- takeCookieJar = lift takeCookieJar- putCookieJar = lift . putCookieJar- readCookieJar = lift readCookieJar
src/Web/Exhentai/Utils.hs view
@@ -1,86 +1,100 @@ {-# LANGUAGE RankNTypes #-} +-- | Internal module module Web.Exhentai.Utils where import Conduit-import Control.Lens+import Control.Effect+import Control.Effect.Bracket+import Control.Effect.Error+import Control.Effect.Exh+import Control.Monad.Trans.Cont import Data.Aeson import Data.ByteString (ByteString)-import Data.Maybe (isNothing) import Data.Text (Text, pack, unpack) import Data.Time-import Network.HTTP.Client.Conduit+import Network.HTTP.Client hiding (Cookie)+import Optics.Core import Text.HTML.DOM-import Text.Read+import Text.Read (readMaybe) import Text.XML hiding (sinkDoc)-import Text.XML.Lens-import Web.Exhentai.Types.CookieT--attributeSatisfies' :: Name -> (Maybe Text -> Bool) -> Traversal' Element Element-attributeSatisfies' n p = filtered (p . preview (attrs . ix n))--withoutAttribute :: Name -> Traversal' Element Element-withoutAttribute = flip attributeSatisfies' isNothing--lower :: Traversal' Element Node-lower = nodes . traverse+import Text.XML.Optics+import Prelude hiding ((!!)) body :: Traversal' Document Element-body = root . named "html" ... named "body"+body = (root % named "html") .// named "body"+{-# INLINE body #-} -div :: Traversal' Element Element+div :: AffineTraversal' Element Element div = named "div"+{-# INLINE div #-} -h1 :: Traversal' Element Element+h1 :: AffineTraversal' Element Element h1 = named "h1"+{-# INLINE h1 #-} -a :: Traversal' Element Element+a :: AffineTraversal' Element Element a = named "a"+{-# INLINE a #-} -table :: Traversal' Element Element+table :: AffineTraversal' Element Element table = named "table"+{-# INLINE table #-} -tr :: Traversal' Element Element+tr :: AffineTraversal' Element Element tr = named "tr"+{-# INLINE tr #-} -td :: Traversal' Element Element+td :: AffineTraversal' Element Element td = named "td"+{-# INLINE td #-} -img :: Traversal' Element Element+img :: AffineTraversal' Element Element img = named "img"+{-# INLINE img #-} -cl :: Text -> Traversal' Element Element+cl :: Text -> AffineTraversal' Element Element cl = attributeIs "class"+{-# INLINE cl #-} -id :: Text -> Traversal' Element Element+id :: Text -> AffineTraversal' Element Element id = attributeIs "id"+{-# INLINE id #-} viaShowRead :: (Show a, Read a) => Prism' Text a viaShowRead = prism' (pack . show) (readMaybe . unpack)+{-# INLINE viaShowRead #-} -scripts :: Traversal' Element Element-scripts = named "script" . attributeIs "type" "text/javascript"+scripts :: AffineTraversal' Element Element+scripts = named "script" % attributeIs "type" "text/javascript"+{-# INLINE scripts #-} infixl 8 ^?: -(^?:) :: Document -> Fold Element a -> Maybe a-doc ^?: fld = doc ^? body ... fld+(^?:) :: (Is (Join A_Traversal l) A_Fold, Is l (Join A_Traversal l), Is A_Traversal (Join A_Traversal l)) => Document -> Optic l is Element Element a a -> Maybe a+doc ^?: fld = doc ^? pre (body .// fld)+{-# INLINE (^?:) #-} infixl 8 ^..: -(^..:) :: Document -> Fold Element a -> [a]-doc ^..: fld = doc ^.. body ... fld+(^..:) :: (Is (Join A_Traversal l) A_Fold, Is l (Join A_Traversal l), Is A_Traversal (Join A_Traversal l)) => Document -> Optic l is Element Element a a -> [a]+doc ^..: fld = doc ^.. body .// fld+{-# INLINE (^..:) #-} sinkAeson :: (FromJSON a, Monad m) => ConduitT ByteString o m (Either String a) sinkAeson = eitherDecode <$> sinkLazy -jsonRequest :: (FromJSON a, MonadHttpState m) => Request -> m (Either String a)-jsonRequest req = withJar req $ \source -> runConduit $ source .| sinkAeson+jsonRequest :: (FromJSON a, Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m) => Request -> m (Either String a)+jsonRequest req = evalContT $ do+ resp <- withSource req+ lift $ runConduitIO $ responseBody resp .| sinkAeson -htmlRequest :: MonadHttpState m => Request -> m Document-htmlRequest req = withJar req $ \source -> runConduit $ source .| sinkDoc+htmlRequest :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => Request -> m Document+htmlRequest req = evalContT $ do+ resp <- withSource req+ lift $ runConduitIO $ responseBody resp .| sinkDoc -htmlRequest' :: MonadHttpState m => Text -> m Document+htmlRequest' :: Effs '[Http, Error HttpException, ConduitIO, Cookie, Bracket] m => Text -> m Document htmlRequest' url = do req <- formRequest $ unpack url htmlRequest req@@ -91,3 +105,14 @@ annotate :: ann -> Maybe a -> Either ann a annotate _ (Just a') = Right a' annotate ann Nothing = Left ann++(!!) :: [a] -> Int -> Maybe a+l !! i+ | i == 0,+ (x : _) <- l =+ Just x+ | i > 0,+ (_ : xs) <- l =+ xs !! (i - 1)+ | otherwise = Nothing+{-# INLINE (!!) #-}
test/Spec.hs view
@@ -1,42 +1,21 @@ module Main where -import Conduit-import Control.Monad.Trans.Cont-import Control.Retry import Data.Either import Data.Maybe-import Network.HTTP.Client+import Optics.Core import Test.Hspec import Text.HTML.DOM-import Web.Exhentai.API.Auth import Web.Exhentai.API.Gallery-import Web.Exhentai.API.MPV import qualified Web.Exhentai.Parsing.Gallery as G import Web.Exhentai.Parsing.Image import qualified Web.Exhentai.Parsing.Search as S-import Web.Exhentai.Types-import Web.Exhentai.Types.CookieT import Web.Exhentai.Utils import Prelude hiding (readFile) main :: IO () main = do sanityCheck- runCookieT retryPolicyDefault $ do- auth $ Credential "VictorYu" "Victor142857"- vars <- fetchMpv $ Gallery 1815207 "5f4ceed6d7"- withSinkFile "1.png" $ \sinkF -> evalContT $ do- resp <- fetchImage $ head $ toRequests vars- let source = responseBody resp- runConduit $ source .| sinkF -{-- withSinkFile "2.zip" $ \sinkF -> evalContT $ do- resp <- streamOriginal "https://exhentai.org/archiver.php?gid=1800698&token=3d413c962e&or=447251--2c3a0356a675f5eb4c5a2afe08b9f15e385647bb"- let source = responseBody resp- runConduit $ source .| sinkF--}- sanityCheck :: IO () sanityCheck = do image <- readFile "test/Image.html"@@ -52,7 +31,7 @@ describe "Image.nextPage" $ do it "should return the link to the next page" $ do- (image ^?: nextPage) `shouldSatisfy` isJust+ (image ^?: nextImage) `shouldSatisfy` isJust describe "Gallery.enTitle" $ do it "should return the title of the gallery" $ do@@ -113,4 +92,4 @@ describe "Search.galleryLink" $ do it "should return galleries" $ do- (search ^?: S.galleryPreviewElement . S.galleryLink) `shouldSatisfy` isJust+ (search ^?: S.galleryPreviewElement % S.galleryLink) `shouldSatisfy` isJust