hscdio-0.1.0.0: src/Sound/Libcdio/Types/Cdio.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-|
Description: Internal module to allow constructor access without pollution.
Copyright: (c) 2019-2021 Sam May
License: GPL-3.0-or-later
Maintainer: ag@eitilt.life
Stability: stable
Portability: non-portable (requires libcdio)
-}
module Sound.Libcdio.Types.Cdio
( Cdio
, CdioError ( .. )
, CdioErrorType ( .. )
, Foreign.SessionArg ( .. )
, Foreign.AccessMode ( .. )
, liftCdio
, liftCdio'
, liftCdioError
, liftCdioError'
, packCdioError
, packCdioError'
, open
, openMode
, getArg
, getAccessMode
, runCdio
) where
import qualified Control.Applicative as A
#if MIN_VERSION_mtl(2,2,1)
import qualified Control.Monad.Except as N.E
#else
import qualified Control.Monad.Error as N.E
#endif
import qualified Control.Monad.Fail as N.F
import qualified Data.Text as T
import qualified Foreign.Libcdio.Device as Foreign
import qualified Foreign.Libcdio.Logging as Foreign
import Sound.Libcdio.Logging
import Control.Applicative ( (<|>) )
import Data.Array.BitArray ( (!) )
-- | A computation within the environment of the data (music or file) stored on
-- a CD. The options for affecting that environment from within are limited by
-- design, as this library is intended for /reading/ discs rather than
-- /authoring/ them.
newtype Cdio a = Cdio (Foreign.Cdio -> IO (Either CdioError a))
instance Functor Cdio where
fmap f (Cdio a) = Cdio $ \c -> fmap f <$> a c
instance Applicative Cdio where
pure a = Cdio . const . return $ Right a
Cdio f <*> Cdio a = Cdio $ \c -> do
f' <- f c
a' <- a c
return $ f' <*> a'
-- | 'A.empty' fails with 'CdioEmpty'.
instance A.Alternative Cdio where
empty = Cdio $ \_ -> return . Left . CdioError CdioEmpty $ T.pack "empty"
Cdio f <|> Cdio g = Cdio $ \c -> f c <|> g c
instance Monad Cdio where
Cdio a >>= f = Cdio $ \c -> do
a' <- a c
case a' of
Left e -> return $ Left e
Right x -> let Cdio b = f x in b c
-- | Wraps the text in a 'FreeformCdioError', for recovery with 'N.E.catchError'.
instance N.F.MonadFail Cdio where
fail e = Cdio . const . return . flip errorText "fail" . FreeformCdioError $ T.pack e
instance N.E.MonadError CdioError Cdio where
throwError err = Cdio . const . return $ Left err
catchError (Cdio f) e = Cdio $ \c -> f c >>= \a' -> case a' of
Left err ->
let Cdio g = e err
in g c
Right a -> return $ Right a
instance LibcdioLogger Cdio where
logCutoff = Cdio $ \_ -> Right <$> Foreign.logCutoff
setLogCutoff l = Cdio $ \_ -> Right <$> Foreign.setLogCutoff l
readLog = Cdio $ \_ -> Right <$> Foreign.readLog
clearLog = Cdio $ \_ -> Right <$> Foreign.clearLog
putLog e = Cdio $ \_ -> Right <$> Foreign.putLog e
-- | Lift a computation from the C-style "Foreign.Libcdio" interface into the
-- monadic "Sound.Libcdio".
liftCdio :: (Foreign.Cdio -> IO a) -> Cdio a
liftCdio f = Cdio $ fmap Right . f
-- | As 'liftCdio', but for functions which take a second argument alongside
-- the read session.
liftCdio' :: (Foreign.Cdio -> a -> IO b) -> a -> Cdio b
liftCdio' f a = Cdio $ fmap Right . flip f a
-- | As 'liftCdio', but for functions which may return an error code indicating
-- failure.
liftCdioError :: (Foreign.Cdio -> IO (Either CdioError a)) -> Cdio a
{-# INLINE liftCdioError #-}
liftCdioError = Cdio
-- | As 'liftCdio'', but for functions which may return an error code indicating
-- failure.
liftCdioError' :: (Foreign.Cdio -> a -> IO (Either CdioError b)) -> a -> Cdio b
liftCdioError' f a = Cdio $ flip f a
-- | Associates a well-typed error with human-readable context information.
data CdioError = CdioError CdioErrorType T.Text
deriving ( Eq, Show, Read )
-- | Potential situations which may cause a computation to fail.
data CdioErrorType
= DriverError
-- ^ A requested operation failed for some unknown reason, or the
-- operating system doesn't support the 'Sound.Libcdio.Device.DriverId'
-- in use.
| BadParameter
-- ^ Some value passed to a requested operation was rejected as
-- nonsensical or otherwise breaking the value-level invariants.
| NotPermitted
-- ^ The ability to perform a requested operation has been restricted
-- (e.g., the user doesn't have permission to access the disc drive).
| SessionClosed
-- ^ The underlying library closed the 'Cdio' session prematurely.
| Unsupported
-- ^ A requested operation isn't available with driver used by the
-- 'Cdio' session. Refer to 'Sound.Libcdio.Device.capabilities' and
-- 'Sound.Libcdio.Device.deviceCapabilities' to reduce these.
| CdioEmpty
-- ^ 'A.empty' was called and no better alternative was encountered.
| FreeformCdioError T.Text
-- ^ Escape hatch from structured typing to allow user-specified
-- (and user-triggered) errors.
deriving ( Eq, Show, Read )
errorText :: CdioErrorType -> String -> Either CdioError a
errorText e = Left . CdioError e . T.pack
packCdioError :: String -> Foreign.DriverReturnCode -> a -> Either CdioError a
packCdioError _ Foreign.Success = Right
packCdioError s Foreign.DriverError = const $ errorText DriverError s
packCdioError s Foreign.Unsupported = const $ errorText Unsupported s
packCdioError s Foreign.Uninitialized = const $ errorText BadParameter s
packCdioError s Foreign.NotPermitted = const $ errorText NotPermitted s
packCdioError s Foreign.BadParameter = const $ errorText BadParameter s
packCdioError s Foreign.BadPointer = const $ errorText BadParameter s
packCdioError s Foreign.NoDriver = const $ errorText DriverError s
packCdioError _ Foreign.MmcSenseData = Right
packCdioError' :: String -> Either Foreign.DriverReturnCode a -> Either CdioError a
packCdioError' _ (Right a) = Right a
packCdioError' s (Left Foreign.Success) = errorText DriverError s
packCdioError' s (Left e) = packCdioError s e undefined
-- | Use a C-style @"Foreign.Libcdio".'Foreign.Cdio'@ object as the base to run
-- a Haskell-style @"Sound.Libcdio".'Cdio'@ computation.
--
-- Note that some invariants of the monadic interface may not work as expected
-- when used with the mutable objects.
runCdio :: Foreign.Cdio -> Cdio a -> IO (Either CdioError a)
runCdio c (Cdio f) = isolateLogs $ f c
-- | Open a session to read data from the disc drive/image at the given
-- location. If passed 'Nothing' instead, uses the path considered "default";
-- on operating systems with a concept of numbered devices (e.g., Window's @D:@
-- drive, FreeBSD's @\/dev\/cd0@) will usually return the first such device
-- found to be suitable.
open
:: Maybe FilePath
-> Bool
-- ^ Whether the disc should be ejected after the computation.
-> Cdio a
-> IO (Either CdioError a)
open = open' "open" Nothing
-- | Open a session to read data from the disc drive/image at the given
-- location, using a specific instruction set. If passed 'Nothing' instead,
-- uses the path considered "default"; on operating systems with a concept of
-- numbered devices (e.g., Window's @D:@ drive, FreeBSD's @\/dev\/cd0@) will
-- usually return the first such device found to be suitable.
openMode
:: Foreign.AccessMode
-> Maybe FilePath
-> Bool
-- ^ Whether the disc should be ejected after the computation.
-> Cdio a
-> IO (Either CdioError a)
openMode = open' "openMode" . Just
open'
:: String
-- ^ The name of the calling function, for error reporting.
-> Maybe Foreign.AccessMode
-> Maybe FilePath
-> Bool
-- ^ Whether the disc should be ejected after the computation.
-> Cdio a
-> IO (Either CdioError a)
open' s m p e (Cdio f) = isolateLogs $ do
-- Rely on the driver autodetect behaviour.
c <- case m of
Just m' -> Foreign.cdioOpenAm p Foreign.DriverUnknown m'
Nothing -> Foreign.cdioOpen p Foreign.DriverUnknown
case c of
Just c' -> do
a <- f c'
-- This isn't actually necessary since the failure code is ignored,
-- but it's a small cost to be sure nothing weird happens (e.g.
-- printing a warning message).
(_, _, cap) <- Foreign.driveCap c'
let e' = if e && cap ! Foreign.MiscEject
then Foreign.ejectMedia c'
else return Foreign.Success
e' >> return a
-- There may be a chance memory allocation fails, but far and away the
-- most common is a path which is not actually a device.
Nothing -> do
return $ errorText BadParameter s
-- | Retrieve the session value associated with the given key. The particular
-- case of @"access-mode"@ is instead handled by 'getAccessMode'.
getArg :: Foreign.SessionArg -> Cdio (Maybe T.Text)
getArg k = liftCdio $ \c -> do
v <- Foreign.getArg c k
return $ T.pack <$> v
-- | Check what instruction set is in use for reading the disc. Other session
-- values are handled by 'getArg'.
getAccessMode :: Cdio (Maybe Foreign.AccessMode)
getAccessMode = liftCdio Foreign.getAccessMode