-- |
--
-- Copyright:
--   This file is part of the package vimeta. It is subject to the
--   license terms in the LICENSE file found in the top-level
--   directory of this distribution and at:
--
--     https://github.com/pjones/vimeta
--
--   No part of this package, including this file, may be copied,
--   modified, propagated, or distributed except according to the terms
--   contained in the LICENSE file.
--
-- License: BSD-2-Clause
--
-- Utility functions for downloading files.
module Vimeta.Core.Download
  ( withArtwork,
    withDownload,
  )
where

import qualified Data.ByteString.Lazy as LByteString
import qualified Data.Text as Text
import qualified Network.HTTP.Client as HC
import System.FilePath
import System.IO (hFlush)
import System.IO.Temp (withSystemTempFile)
import Vimeta.Core.Config
import Vimeta.Core.Vimeta

-- | Try to download artwork and run the given function.  The
-- function will be passed a 'FilePath' if the artwork was downloaded.
--
-- See the 'withDownload' function for more details.
withArtwork ::
  (MonadIO m) =>
  [Text] ->
  (Maybe FilePath -> Vimeta IO a) ->
  Vimeta m a
withArtwork :: [Text] -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
withArtwork [Text]
urls = Maybe Text -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
Maybe Text -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
withDownload ([Text] -> Maybe Text
forall a. [a] -> Maybe a
listToMaybe ([Text] -> Maybe Text) -> [Text] -> Maybe Text
forall a b. (a -> b) -> a -> b
$ [Text] -> [Text]
candidates [Text]
urls)
  where
    candidates :: [Text] -> [Text]
    candidates :: [Text] -> [Text]
candidates = (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter Text -> Bool
checkExtension ([Text] -> [Text]) -> ([Text] -> [Text]) -> [Text] -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Text] -> [Text]
forall a. [a] -> [a]
reverse
    checkExtension :: Text -> Bool
    checkExtension :: Text -> Bool
checkExtension = FilePath -> Bool
goodExtension (FilePath -> Bool) -> (Text -> FilePath) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. FilePath -> FilePath
takeExtension (FilePath -> FilePath) -> (Text -> FilePath) -> Text -> FilePath
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> FilePath
forall a. ToString a => a -> FilePath
toString (Text -> FilePath) -> (Text -> Text) -> Text -> FilePath
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
Text.toLower
    goodExtension :: String -> Bool
    goodExtension :: FilePath -> Bool
goodExtension FilePath
ext = FilePath
ext FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== FilePath
".jpg" Bool -> Bool -> Bool
|| FilePath
ext FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== FilePath
".png"

-- | Download the given URL to a temporary file and pass the file
-- name to the given function.
--
-- The reason a function needs to be passed to 'withDownload' is the
-- result of using 'withSystemTempFile' to store the downloaded file.
-- The file will be automatically removed after the given function
-- completes.
withDownload ::
  (MonadIO m) =>
  -- | URL.
  Maybe Text ->
  -- | Function to call and pass the file name to.
  (Maybe FilePath -> Vimeta IO a) ->
  -- | Result of above function.
  Vimeta m a
withDownload :: Maybe Text -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
withDownload Maybe Text
Nothing Maybe FilePath -> Vimeta IO a
f = do
  Text -> Vimeta m ()
forall (m :: * -> *). MonadIO m => Text -> Vimeta m ()
verbose Text
"no URL to download"
  IO (Either FilePath a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
IO (Either FilePath a) -> Vimeta m a
runIOE (IO (Either FilePath a) -> Vimeta m a)
-> IO (Either FilePath a) -> Vimeta m a
forall a b. (a -> b) -> a -> b
$ Vimeta IO a -> IO (Either FilePath a)
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
Vimeta m a -> m (Either FilePath a)
runVimeta (Maybe FilePath -> Vimeta IO a
f Maybe FilePath
forall a. Maybe a
Nothing)
withDownload Maybe Text
url Maybe FilePath -> Vimeta IO a
f = do
  Context
context <- Vimeta m Context
forall r (m :: * -> *). MonadReader r m => m r
ask

  let dryRun :: Bool
dryRun = Config -> Bool
configDryRun (Config -> Bool) -> Config -> Bool
forall a b. (a -> b) -> a -> b
$ Context -> Config
ctxConfig Context
context
      manager :: Manager
manager = Context -> Manager
ctxManager Context
context

  case (Bool
dryRun, Maybe Text
url) of
    (Bool
True, Maybe Text
Nothing) ->
      Text -> Vimeta m ()
forall (m :: * -> *). MonadIO m => Text -> Vimeta m ()
verbose Text
"dry-run: nothing to download"
        Vimeta m () -> Vimeta m a -> Vimeta m a
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
(Maybe FilePath -> Vimeta IO a) -> Vimeta m a
runWithoutTempFile Maybe FilePath -> Vimeta IO a
f
    (Bool
False, Maybe Text
Nothing) ->
      Text -> Vimeta m ()
forall (m :: * -> *). MonadIO m => Text -> Vimeta m ()
verbose Text
"nothing to download"
        Vimeta m () -> Vimeta m a -> Vimeta m a
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
(Maybe FilePath -> Vimeta IO a) -> Vimeta m a
runWithoutTempFile Maybe FilePath -> Vimeta IO a
f
    (Bool
True, Just Text
u) ->
      Text -> Vimeta m ()
forall (m :: * -> *). MonadIO m => Text -> Vimeta m ()
verbose (Text
"dry-run:" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
u)
        Vimeta m () -> Vimeta m a -> Vimeta m a
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
(Maybe FilePath -> Vimeta IO a) -> Vimeta m a
runWithoutTempFile Maybe FilePath -> Vimeta IO a
f
    (Bool
False, Just Text
u) ->
      Text -> Vimeta m ()
forall (m :: * -> *). MonadIO m => Text -> Vimeta m ()
verbose Text
u
        Vimeta m () -> Vimeta m a -> Vimeta m a
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Text -> Manager -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
Text -> Manager -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
runWithTempFile Text
u Manager
manager Maybe FilePath -> Vimeta IO a
f

-- | Helper function to run the download action with a temporary file.
runWithTempFile ::
  (MonadIO m) =>
  Text ->
  HC.Manager ->
  (Maybe FilePath -> Vimeta IO a) ->
  Vimeta m a
runWithTempFile :: Text -> Manager -> (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
runWithTempFile Text
url Manager
manager Maybe FilePath -> Vimeta IO a
vio = do
  Context
context <- Vimeta m Context
forall r (m :: * -> *). MonadReader r m => m r
ask

  IO (Either FilePath a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
IO (Either FilePath a) -> Vimeta m a
runIOE (IO (Either FilePath a) -> Vimeta m a)
-> IO (Either FilePath a) -> Vimeta m a
forall a b. (a -> b) -> a -> b
$
    FilePath
-> (FilePath -> Handle -> IO (Either FilePath a))
-> IO (Either FilePath a)
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
FilePath -> (FilePath -> Handle -> m a) -> m a
withSystemTempFile FilePath
"vimeta" ((FilePath -> Handle -> IO (Either FilePath a))
 -> IO (Either FilePath a))
-> (FilePath -> Handle -> IO (Either FilePath a))
-> IO (Either FilePath a)
forall a b. (a -> b) -> a -> b
$ \FilePath
name Handle
h -> do
      Manager -> FilePath -> Handle -> IO ()
downloadToHandle Manager
manager (Text -> FilePath
forall a. ToString a => a -> FilePath
toString Text
url) Handle
h
      Context -> Vimeta IO a -> IO (Either FilePath a)
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
Context -> Vimeta m a -> m (Either FilePath a)
execVimetaWithContext Context
context (Vimeta IO a -> IO (Either FilePath a))
-> Vimeta IO a -> IO (Either FilePath a)
forall a b. (a -> b) -> a -> b
$ Maybe FilePath -> Vimeta IO a
vio (FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just FilePath
name)

-- | Helper function to run an action without needing a temporary file.
runWithoutTempFile ::
  (MonadIO m) =>
  (Maybe FilePath -> Vimeta IO a) ->
  Vimeta m a
runWithoutTempFile :: (Maybe FilePath -> Vimeta IO a) -> Vimeta m a
runWithoutTempFile Maybe FilePath -> Vimeta IO a
vio = do
  Context
context <- Vimeta m Context
forall r (m :: * -> *). MonadReader r m => m r
ask
  IO (Either FilePath a) -> Vimeta m a
forall (m :: * -> *) a.
MonadIO m =>
IO (Either FilePath a) -> Vimeta m a
runIOE (IO (Either FilePath a) -> Vimeta m a)
-> IO (Either FilePath a) -> Vimeta m a
forall a b. (a -> b) -> a -> b
$ Context -> Vimeta IO a -> IO (Either FilePath a)
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
Context -> Vimeta m a -> m (Either FilePath a)
execVimetaWithContext Context
context (Vimeta IO a -> IO (Either FilePath a))
-> Vimeta IO a -> IO (Either FilePath a)
forall a b. (a -> b) -> a -> b
$ Maybe FilePath -> Vimeta IO a
vio Maybe FilePath
forall a. Maybe a
Nothing

-- | Helper function to the actual HTTP downloading into a file handle.
downloadToHandle :: HC.Manager -> String -> Handle -> IO ()
downloadToHandle :: Manager -> FilePath -> Handle -> IO ()
downloadToHandle Manager
manager FilePath
url Handle
handle = do
  Request
request <- FilePath -> IO Request
forall (m :: * -> *). MonadThrow m => FilePath -> m Request
HC.parseRequest FilePath
url
  Response ByteString
response <- Request -> Manager -> IO (Response ByteString)
HC.httpLbs Request
request Manager
manager
  Handle -> ByteString -> IO ()
LByteString.hPut Handle
handle (Response ByteString -> ByteString
forall body. Response body -> body
HC.responseBody Response ByteString
response)
  Handle -> IO ()
hFlush Handle
handle