packages feed

pixiv-0.1.0: src/Web/Pixiv/Download.hs

-- | Copyright: (c) 2021 The closed eye of love
-- SPDX-License-Identifier: BSD-3-Clause
-- Maintainer: Poscat <poscat@mail.poscat.moe>, berberman <berberman@yandex.com>
-- Stability: alpha
-- Portability: portable
-- A set of utilities for downloading pixiv things.
module Web.Pixiv.Download
  ( -- * DownloadM monad
    DownloadM,
    liftMaybe,
    liftToPixivT,
    runDownloadM,

    -- * Download actions
    downloadPixiv,
    downloadSingleIllust,
    downloadUgoiraToMP4,
  )
where

import Control.Lens
import Control.Monad.Reader
import Control.Monad.Trans.Maybe
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Network.HTTP.Client
import System.FilePath ((</>))
import System.IO.Temp (getCanonicalTemporaryDirectory, withTempDirectory)
import System.Process
import Web.Pixiv.Types
import Web.Pixiv.Types.Lens
import Web.Pixiv.Types.PixivT
import Web.Pixiv.Utils

-- | 'DownloadM' monad is a synonym for 'IO' computation wrapped in 'Manager' environment,
-- which may exit without producing value, indicating the download failed.
type DownloadM = MaybeT (ReaderT Manager IO)

-- | Lifts a pure 'Maybe' value to 'DownloadM'.
liftMaybe :: Maybe a -> DownloadM a
liftMaybe x = MaybeT . liftIO $ pure x

-- | Executes the download computation in 'IO'.
runDownloadM :: Manager -> DownloadM a -> IO (Maybe a)
runDownloadM manager m = runMaybeT m & flip runReaderT manager

-- | Lifts a download computation to 'PixivT'.
--
-- 'DownloadM' needs 'Manager' to perform the download, which can be provided by 'TokenState'.
liftToPixivT :: (MonadIO m) => DownloadM a -> PixivT m (Maybe a)
liftToPixivT m = do
  PixivState {tokenState = TokenState {..}} <- readPixivState
  liftIO $ runDownloadM manager m

-- | Downloads something in 'DownloadM', given url.
downloadPixiv :: Text -> DownloadM LBS.ByteString
downloadPixiv url = do
  let addReferer r = r {requestHeaders = [("Referer", "https://app-api.pixiv.net/")]}
  manager <- ask
  req <- addReferer <$> (parseRequest . T.unpack $ url)
  resp <- liftIO $ httpLbs req manager
  pure $ responseBody resp

-- | Downloads a single page illust.
--
-- Chooses the first one if the illust has many pages,
-- preferring high quality images. Returns `Nothing` if can't find any image url.
downloadSingleIllust :: Illust -> DownloadM LBS.ByteString
downloadSingleIllust i = do
  url <- liftMaybe $ extractImageUrlsFromIllust i ^? _head
  downloadPixiv url

-- | Downloads 'UgoiraFrame's, then converts it to MP4 calling external @ffmpeg@.
downloadUgoiraToMP4 ::
  -- | Information of ugoira to download
  UgoiraMetadata ->
  -- | Path to @ffmpeg@
  Maybe FilePath ->
  DownloadM (String, LBS.ByteString)
downloadUgoiraToMP4 meta (fromMaybe "ffmpeg" -> ffmpeg) = do
  let ffconcat = ugoiraMetadataToFFConcat meta
  bs0 <- downloadPixiv $ meta ^. zipUrls . zipMedium
  systmp <- liftIO getCanonicalTemporaryDirectory
  liftIO $
    withTempDirectory systmp "ugoira" $ \temp -> do
      unzipArchive temp bs0
      let concatFilePath = temp </> "concat"
          convertProcess =
            ( proc
                ffmpeg
                [ "-y",
                  "-i",
                  "concat",
                  "-c:v",
                  "libx264",
                  "-vf",
                  "pad=ceil(iw/2)*2:ceil(ih/2)*2",
                  "-pix_fmt",
                  "yuv420p",
                  "-lossless",
                  "1",
                  "ugoira.mp4"
                ]
            )
              { cwd = Just temp
              }
      BS.writeFile concatFilePath ffconcat
      (_code, _stdout, stderr) <- readCreateProcessWithExitCode convertProcess ""
      (stderr,) <$> LBS.readFile (temp </> "ugoira.mp4")