exh-1.0.0: src/Web/Exhentai/API/Gallery.hs
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Gallery API.
module Web.Exhentai.API.Gallery
( Gallery (..),
GalleryInfo (..),
Visibility (..),
GalleryCategory (..),
TagCategory (..),
allGalleryCats,
fetchGalleryInfo,
-- ** Internal API
parseGalleryLink,
parseGallery,
)
where
import Control.Effect
import Control.Effect.Bracket
import Control.Effect.Error
import Control.Effect.Exh
import Control.Monad
import Data.Maybe
import Data.Set (Set, fromList)
import Data.Text (Text, pack, strip)
import Data.Time
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.Optics
import Web.Exhentai.Errors
import qualified Web.Exhentai.Parsing.Gallery as G
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 :: GalleryCategory,
jaTitle :: Maybe Text,
uploader :: {-# UNPACK #-} Text,
rating :: {-# UNPACK #-} Double,
ratingCount :: {-# UNPACK #-} Int,
favoriteCount :: {-# UNPACK #-} Int,
tags :: [(TagCategory, [Text])],
uploadTime :: {-# UNPACK #-} UTCTime,
newer :: Maybe Gallery,
parent :: Maybe Gallery,
visibility :: Visibility,
language :: {-# UNPACK #-} Text,
length :: {-# UNPACK #-} Int,
archiverLink :: {-# UNPACK #-} Text,
torrentLink :: {-# UNPACK #-} Text
}
deriving (Show, Eq)
data Visibility
= Visible
| Replaced
| Expunged
| Unknown {-# UNPACK #-} Text
deriving (Show, Eq)
readVisibility :: Text -> Visibility
readVisibility "Yes" = Visible
readVisibility "No (Replaced)" = Replaced
readVisibility "No (Expunged)" = Expunged
readVisibility v = Unknown v
-- | Extract all gallery informations from a document
parseGallery :: Document -> Either Text GalleryInfo
parseGallery d = do
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 >>= readCat
uploader <- annotate "uploader" $ d ^?: G.uploader
rating <- annotate "average rating" $ d ^?: G.averageRating >>= parseAverageRating
ratingCount <- annotate "rating count" $ d ^?: G.ratingCount
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 ^? 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 "length of categories and tags does not match"
let tags = zip cats tags'
favoriteCount <- annotate "favorite count" $ fav ^? pre lower % _Content >>= parseFavoriteCount
pure GalleryInfo {..}
_ -> Left "extracting metadata"
-- | Fetch a gallery's '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 -> 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