packages feed

htaglib 1.2.0 → 1.2.1

raw patch · 13 files changed

+727/−760 lines, 13 filesdep −semigroupsdep −transformersdep ~basedep ~bytestringdep ~filepathsetup-changed

Dependencies removed: semigroups, transformers

Dependency ranges changed: base, bytestring, filepath, hspec, text

Files

CHANGELOG.md view
@@ -1,3 +1,7 @@+## HTagLib 1.2.1++* Maintenance release with more modern and minimal dependencies.+ ## HTagLib 1.2.0  * Compiles with GHC 8.4.1.
LICENSE.md view
@@ -1,4 +1,4 @@-Copyright © 2015–2018 Mark Karpov+Copyright © 2015–present Mark Karpov  All rights reserved. 
README.md view
@@ -4,7 +4,7 @@ [![Hackage](https://img.shields.io/hackage/v/htaglib.svg?style=flat)](https://hackage.haskell.org/package/htaglib) [![Stackage Nightly](http://stackage.org/package/htaglib/badge/nightly)](http://stackage.org/nightly/package/htaglib) [![Stackage LTS](http://stackage.org/package/htaglib/badge/lts)](http://stackage.org/lts/package/htaglib)-[![Build Status](https://travis-ci.org/mrkkrp/htaglib.svg?branch=master)](https://travis-ci.org/mrkkrp/htaglib)+![CI](https://github.com/mrkkrp/htaglib/workflows/CI/badge.svg?branch=master)  * [Alternatives](#alternatives) * [A note for FLAC users](#a-note-for-flac-users)@@ -14,8 +14,8 @@     * [Conclusion](#conclusion) * [License](#license) -This is Haskell bindings to [TagLib](https://taglib.github.io/), library for-reading and editing meta-data of several popular audio formats.+This is Haskell bindings to [TagLib](https://taglib.github.io/), a library+for reading and editing meta-data of several popular audio formats.  It works with the following formats: @@ -35,23 +35,21 @@  ## Alternatives -There is at least two Haskell bindings doing “the same” thing:+There is at least two other Haskell bindings to TagLib:  * [`libtagc`](https://hackage.haskell.org/package/libtagc) * [`taglib`](https://hackage.haskell.org/package/taglib) -Both are very low level, without any protection or higher-level-abstractions, not really type-safe. I personally don't want to use them, so-I wrote this.+Both are low level, without any type safety or higher-level abstractions.  ## A note for FLAC users -If you want to work with FLAC, there is-a [complete Haskell binding](https://github.com/mrkkrp/flac) to-`libFLAC`—reference FLAC implementation. It allows to work with all FLAC-metadata (read and write) and also provides Haskell API to stream encoder-and stream decoder. Please prefer that package if you don't need to work-with other audio formats.+If you want to work with FLAC, there is a [complete Haskell+binding](https://github.com/mrkkrp/flac) to `libFLAC`—the reference FLAC+implementation. It allows us to work with all FLAC metadata (read and write)+and also provides a Haskell API to the stream encoder and the stream+decoder. Please prefer that package if you don't need to work with other+audio formats.  ## Quick start @@ -59,13 +57,6 @@ install the library itself. If you're on a Unix-like system, chances are you'll have it in the official repositories of your distro. -After installation of the library, install `htaglib` package using Cabal or-Stack (recommended):--```-$ stack install htaglib-```- ### Reading meta data  Now to the hacking. It's recommended that you define a record representing@@ -79,25 +70,23 @@ import System.Environment (getArgs)  data AudioTrack = AudioTrack-  { atTitle   :: Title-  , atArtist  :: Artist-  , atAlbum   :: Album-  , atComment :: Comment-  , atGenre   :: Genre-  , atYear    :: Maybe Year-  , atTrack   :: Maybe TrackNumber }-  deriving Show+  { atTitle :: Title,+    atArtist :: Artist,+    atAlbum :: Album,+    atComment :: Comment,+    atGenre :: Genre,+    atYear :: Maybe Year,+    atTrack :: Maybe TrackNumber+  }+  deriving (Show) ``` -A couple of notes here. We use unique types for every component of meta-data, so it's more difficult to use track title in lieu of track artist, for-example. Meta data that is represented by strings also has smart-constructors, they replace zero bytes with spaces, this is necessary to-avoid troubles when your Haskell strings go to the C-level (well, zero-bytes-in strings is rather edge case, but it should be mentioned). Of course,-`Title`, `Artist`, `Album`, `Comment`, and `Genre` all are instances of-`IsString`, so just turn on `OverloadedStrings` and you can use normal-string literals to create data of these types.+We use unique types for every component of meta data, so it's more difficult+to use a track title instead of a track artist, for example. String meta+data types have smart constructors, but `Title`, `Artist`, `Album`,+`Comment`, and `Genre` all are instances of `IsString`, so it is enough to+turn on the `OverloadedStrings` language extension to use normal string+literals to create values of these types.  `Year` and `TrackNumber` may be not set or missing, in this case you get `Nothing`. This is possible with string-based fields too, but in that case@@ -106,26 +95,27 @@  OK, it's time to read some info. There is `TagGetter` type which is an applicative functor. You first construct `TagGetter` which will retrieve-entire `AudioTrack` for you using applicative style:+entire `AudioTrack` for you using the applicative style:  ```haskell audioTrackGetter :: TagGetter AudioTrack-audioTrackGetter = AudioTrack-  <$> titleGetter-  <*> artistGetter-  <*> albumGetter-  <*> commentGetter-  <*> genreGetter-  <*> yearGetter-  <*> trackNumberGetter+audioTrackGetter =+  AudioTrack+    <$> titleGetter+    <*> artistGetter+    <*> albumGetter+    <*> commentGetter+    <*> genreGetter+    <*> yearGetter+    <*> trackNumberGetter ``` -Perfect, now use `getTags` to read entire record:+Perfect, now use `getTags` to read the entire record:  ```haskell main :: IO () main = do-  path  <- head <$> getArgs+  path <- head <$> getArgs   track <- getTags path audioTrackGetter   print track ```@@ -135,13 +125,13 @@ ``` $ ./example "/home/mark/music/David Bowie/1977, Low/01 Speed of Life.flac" AudioTrack-  { atTitle   = Title   "Speed of Life"-  , atArtist  = Artist  "David Bowie"-  , atAlbum   = Album   "Low"-  , atComment = Comment ""-  , atGenre   = Genre   ""-  , atYear    = Just    (Year 1977)-  , atTrack   = Just    (TrackNumber 1)+  { atTitle = Title "Speed of Life",+    atArtist = Artist "David Bowie",+    atAlbum = Album "Low",+    atComment = Comment "",+    atGenre = Genre "",+    atYear = Just (Year 1977),+    atTrack = Just (TrackNumber 1)   } ``` @@ -149,49 +139,42 @@ etc. but it's not shown here for simplicity, consult Haddocks for more information. -N.B. If you need to extract duration of tracks, TagLib only returns number-of seconds as an integer. This means that if you want to calculate total-duration, you'll have slightly incorrect result. Proper solution is to-extract duration as floating-point number, for that we recommend bindings to+N.B. If you need to extract the duration, TagLib only returns the number of+seconds as an integer. This means that if you want to calculate the total+duration of an album, you'll get a slightly incorrect result. The right+solution is to extract the duration as floating-point number, for that we+recommend the bindings to `libsndfile`—[`hsndfile`](https://hackage.haskell.org/package/hsndfile) (or the above-mentioned `flac` package for Haskell if you work with FLAC).  ### Writing meta data -We cannot use applicative interface to set tags. There are several reasons:--* Applicative interface in general is better for extracting or parsing (or-  rather assembling complex parsers from more basic ones).--* Some fields like sample rate or length can only be read, not set.--* We may wish to set one or two fields selectively, not everything.--Solution: use monoids. `TagSetter` is an instance of `Monoid`. This means-that we can set title and artist of audio track like this:+`TagSetter` is an instance of `Monoid`. This means that we can set title and+artist of audio track like this:  ```haskell main :: IO () main = do   (path : title : artist : _) <- getArgs   setTags path Nothing $-    titleSetter (mkTitle title) <>-    artistSetter (mkArtist artist)+    titleSetter (mkTitle title)+      <> artistSetter (mkArtist artist)   track <- getTags path audioTrackGetter   print track ``` -This code loads file and changes “title” and “artist” meta data fields.+This code loads a file and changes the “title” and “artist” meta data+fields. -## Conclusion+## Contribution -With the interface provided by `getTags` and `setTags` it's not possible to-forget to close file or free some resource. You can read all meta data at-once directly into your data structure in type-safe manner. Writing meta-data should be trivial too. Have fun!+Issues, bugs, and questions may be reported in [the GitHub issue tracker for+this project](https://github.com/mrkkrp/htaglib/issues). +Pull requests are also welcome.+ ## License -Copyright © 2015–2018 Mark Karpov+Copyright © 2015–present Mark Karpov  Distributed under BSD 3 clause license.
− Setup.hs
@@ -1,6 +0,0 @@-module Main (main) where--import Distribution.Simple--main :: IO ()-main = defaultMain
Sound/HTagLib.hs view
@@ -1,79 +1,81 @@ -- | -- Module      :  Sound.HTagLib--- Copyright   :  © 2015–2018 Mark Karpov+-- Copyright   :  © 2015–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com> -- Stability   :  experimental -- Portability :  portable ----- This module provides a complete high-level interface to TagLib. This is--- the module you should import to use in your projects.-+-- This module provides an interface to TagLib. This is the module you+-- should import to use in your projects. module Sound.HTagLib   ( -- * Getters-    getTags-  , getTags'-  , titleGetter-  , artistGetter-  , albumGetter-  , commentGetter-  , genreGetter-  , yearGetter-  , trackNumberGetter-  , durationGetter-  , bitRateGetter-  , sampleRateGetter-  , channelsGetter+    getTags,+    getTags',+    titleGetter,+    artistGetter,+    albumGetter,+    commentGetter,+    genreGetter,+    yearGetter,+    trackNumberGetter,+    durationGetter,+    bitRateGetter,+    sampleRateGetter,+    channelsGetter,+     -- * Setters-  , setTags-  , setTags'-  , titleSetter-  , artistSetter-  , albumSetter-  , commentSetter-  , genreSetter-  , yearSetter-  , trackNumberSetter+    setTags,+    setTags',+    titleSetter,+    artistSetter,+    albumSetter,+    commentSetter,+    genreSetter,+    yearSetter,+    trackNumberSetter,+     -- * Data types-  , Title-  , mkTitle-  , unTitle-  , Artist-  , mkArtist-  , unArtist-  , Album-  , mkAlbum-  , unAlbum-  , Comment-  , mkComment-  , unComment-  , Genre-  , mkGenre-  , unGenre-  , Year-  , mkYear-  , unYear-  , TrackNumber-  , mkTrackNumber-  , unTrackNumber-  , Duration-  , mkDuration-  , unDuration-  , BitRate-  , mkBitRate-  , unBitRate-  , SampleRate-  , mkSampleRate-  , unSampleRate-  , Channels-  , mkChannels-  , unChannels-  , FileType (..)-  , ID3v2Encoding (..)-  , TagGetter-  , TagSetter-  , HTagLibException (..) )+    Title,+    mkTitle,+    unTitle,+    Artist,+    mkArtist,+    unArtist,+    Album,+    mkAlbum,+    unAlbum,+    Comment,+    mkComment,+    unComment,+    Genre,+    mkGenre,+    unGenre,+    Year,+    mkYear,+    unYear,+    TrackNumber,+    mkTrackNumber,+    unTrackNumber,+    Duration,+    mkDuration,+    unDuration,+    BitRate,+    mkBitRate,+    unBitRate,+    SampleRate,+    mkSampleRate,+    unSampleRate,+    Channels,+    mkChannels,+    unChannels,+    FileType (..),+    ID3v2Encoding (..),+    TagGetter,+    TagSetter,+    HTagLibException (..),+  ) where  import Sound.HTagLib.Getter
Sound/HTagLib/Getter.hs view
@@ -1,145 +1,139 @@+{-# LANGUAGE DeriveFunctor #-}+ -- | -- Module      :  Sound.HTagLib.Getter--- Copyright   :  © 2015–2018 Mark Karpov+-- Copyright   :  © 2015–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com> -- Stability   :  experimental -- Portability :  portable ----- A high-level applicative interface for reading of audio meta data. You--- don't need to import this module directly, import "Sound.HTagLib"--- instead.--{-# LANGUAGE CPP #-}-+-- An applicative interface for reading of audio meta data. You don't need+-- to import this module directly, import "Sound.HTagLib" instead. module Sound.HTagLib.Getter   ( -- * High-level API-    TagGetter-  , getTags-  , getTags'+    TagGetter,+    getTags,+    getTags',+     -- * Built-in getters-  , titleGetter-  , artistGetter-  , albumGetter-  , commentGetter-  , genreGetter-  , yearGetter-  , trackNumberGetter-  , durationGetter-  , bitRateGetter-  , sampleRateGetter-  , channelsGetter )+    titleGetter,+    artistGetter,+    albumGetter,+    commentGetter,+    genreGetter,+    yearGetter,+    trackNumberGetter,+    durationGetter,+    bitRateGetter,+    sampleRateGetter,+    channelsGetter,+  ) where  import Control.Monad.IO.Class+import Sound.HTagLib.Internal qualified as I import Sound.HTagLib.Type-import qualified Sound.HTagLib.Internal as I -#if !MIN_VERSION_base(4,8,0)-import Control.Applicative-#endif---- | A composable entity that can be used with 'getTags' or 'getTags''--- functions to read batch of meta parameters.--newtype TagGetter a = TagGetter { runGetter :: I.FileId -> IO a }--instance Functor TagGetter where-  fmap f x = TagGetter $ \fid -> f <$> runGetter x fid+-- | A composable entity that can be passed to the 'getTags' or 'getTags''+-- functions to read multiple meta data fields at once.+newtype TagGetter a = TagGetter {runGetter :: I.FileId -> IO a}+  deriving (Functor)  instance Applicative TagGetter where-  pure    = TagGetter . const . return-  x <*> y = TagGetter $ \fid ->-    do f <- runGetter x fid-       f <$> runGetter y fid+  pure = TagGetter . const . return+  x <*> y = TagGetter $ \fid -> do+    f <- runGetter x fid+    f <$> runGetter y fid --- | @getTags path g@ will try to read file located at @path@ and read meta--- data of the file using getter @g@. Type of file will be guessed from its--- extension. If this is not satisfactory and you want to explicitly specify--- the file type, see 'getTags'' variation of this function.+-- | @getTags path g@ will try to read the file located at @path@ and read+-- the meta data of the file using the getter @g@. The type of the file will+-- be guessed from its extension. If this is not satisfactory and you want+-- to explicitly specify the file type, see 'getTags'' variation of this+-- function. ----- In the case of trouble 'I.HTagLibException' will be thrown.--getTags :: MonadIO m-  => FilePath          -- ^ Path to audio file-  -> TagGetter a       -- ^ Getter-  -> m a              -- ^ Extracted data+-- Throws 'I.HTagLibException'.+getTags ::+  (MonadIO m) =>+  -- | Path to audio file+  FilePath ->+  -- | Getter+  TagGetter a ->+  -- | Extracted data+  m a getTags path = execGetter path Nothing --- | This is essentially the same as 'getTags', but allows to explicitly+-- | This is essentially the same as 'getTags', but allows us to explicitly -- choose file type (see 'FileType').--getTags' :: MonadIO m-  => FilePath          -- ^ Path to audio file-  -> FileType          -- ^ Type of audio file-  -> TagGetter a       -- ^ Getter-  -> m a               -- ^ Extracted data+getTags' ::+  (MonadIO m) =>+  -- | Path to audio file+  FilePath ->+  -- | Type of audio file+  FileType ->+  -- | Getter+  TagGetter a ->+  -- | Extracted data+  m a getTags' path t = execGetter path (Just t)  -- | This is the most general way to read meta data from file. 'getTags' and -- 'getTags'' are just wrappers around the function.--execGetter :: MonadIO m-  => FilePath         -- ^ Path to audio file-  -> Maybe FileType   -- ^ Type of audio file (if known)-  -> TagGetter a      -- ^ Getter-  -> m a              -- ^ Extracted data+execGetter ::+  (MonadIO m) =>+  -- | Path to audio file+  FilePath ->+  -- | Type of audio file (if known)+  Maybe FileType ->+  -- | Getter+  TagGetter a ->+  -- | Extracted data+  m a execGetter path t = liftIO . I.withFile path t . runGetter --- | Getter to retrieve track title.-+-- | Getter to retrieve the track title. titleGetter :: TagGetter Title titleGetter = TagGetter I.getTitle --- | Getter to retrieve track artist.-+-- | Getter to retrieve the track artist. artistGetter :: TagGetter Artist artistGetter = TagGetter I.getArtist --- | Getter to retrieve track album.-+-- | Getter to retrieve the track album. albumGetter :: TagGetter Album albumGetter = TagGetter I.getAlbum --- | Getter to retrieve track comment.-+-- | Getter to retrieve the track comment. commentGetter :: TagGetter Comment commentGetter = TagGetter I.getComment --- | Getter to retrieve genre of the track.-+-- | Getter to retrieve the genre of the track. genreGetter :: TagGetter Genre genreGetter = TagGetter I.getGenre --- | Getter to retrieve year to the track (returns 'Nothing' if the data is--- missing).-+-- | Getter to retrieve the year of the track (returns 'Nothing' if the data+-- is missing). yearGetter :: TagGetter (Maybe Year) yearGetter = TagGetter I.getYear --- | Getter to retrieve track number (returns 'Nothing' if the data is+-- | Getter to retrieve the track number (returns 'Nothing' if the data is -- missing).- trackNumberGetter :: TagGetter (Maybe TrackNumber) trackNumberGetter = TagGetter I.getTrackNumber --- | Getter to retrieve duration in seconds.-+-- | Getter to retrieve the duration in seconds. durationGetter :: TagGetter Duration durationGetter = TagGetter I.getDuration --- | Getter to retrieve bit rate.-+-- | Getter to retrieve the bit rate. bitRateGetter :: TagGetter BitRate bitRateGetter = TagGetter I.getBitRate --- | Getter to retrieve sample rate.-+-- | Getter to retrieve the sample rate. sampleRateGetter :: TagGetter SampleRate sampleRateGetter = TagGetter I.getSampleRate --- | Getter to retrieve number of channels in audio data.-+-- | Getter to retrieve the number of channels of the audio data. channelsGetter :: TagGetter Channels channelsGetter = TagGetter I.getChannels
Sound/HTagLib/Internal.hs view
@@ -1,51 +1,53 @@+{-# LANGUAGE ForeignFunctionInterface #-}+ -- | -- Module      :  Sound.HTagLib.Internal--- Copyright   :  © 2015–2018 Mark Karpov+-- Copyright   :  © 2015–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com> -- Stability   :  experimental -- Portability :  portable ----- Low-level interaction with underlying C API. You don't want to use this,--- see "Sound.HTagLib" instead.--{-# LANGUAGE CPP                      #-}-{-# LANGUAGE EmptyDataDecls           #-}-{-# LANGUAGE ForeignFunctionInterface #-}-+-- Low-level interaction with the underlying C API. You don't want to use+-- this, see "Sound.HTagLib" instead. module Sound.HTagLib.Internal   ( -- * Data types-    FileId+    FileId,+     -- * File API-  , withFile-  , saveFile+    withFile,+    saveFile,+     -- * Tag API-  , getTitle-  , getArtist-  , getAlbum-  , getComment-  , getGenre-  , getYear-  , getTrackNumber-  , setTitle-  , setArtist-  , setAlbum-  , setComment-  , setGenre-  , setYear-  , setTrackNumber+    getTitle,+    getArtist,+    getAlbum,+    getComment,+    getGenre,+    getYear,+    getTrackNumber,+    setTitle,+    setArtist,+    setAlbum,+    setComment,+    setGenre,+    setYear,+    setTrackNumber,+     -- * Audio properties API-  , getDuration-  , getBitRate-  , getSampleRate-  , getChannels+    getDuration,+    getBitRate,+    getSampleRate,+    getChannels,+     -- * Special convenience ID3v2 functions-  , id3v2SetEncoding )+    id3v2SetEncoding,+  ) where -import Control.Exception (throw, bracket)-import Control.Monad (when, unless)+import Control.Exception (bracket, throw)+import Control.Monad (unless, when) import Data.ByteString (packCString, useAsCString) import Data.Maybe (fromJust) import Data.Text (Text)@@ -53,19 +55,16 @@ import Foreign import Foreign.C.String import Foreign.C.Types-import qualified Sound.HTagLib.Type as T--#if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<$>))-#endif+import Sound.HTagLib.Type qualified as T  data TagLibFile+ data TagLibTag+ data TagLibProperties  -- | This is an abstraction representing opened file. Other modules can -- pass it around and treat it like a black box.- newtype FileId = FileId (Ptr TagLibFile)  ----------------------------------------------------------------------------@@ -166,19 +165,23 @@ ---------------------------------------------------------------------------- -- File API --- | Open audio file and return its ID (an opaque type that the rest of--- library can pass around). In case of trouble 'IOException' is thrown.--newFile-  :: FilePath          -- ^ Path to audio file-  -> Maybe T.FileType  -- ^ Type of file (or it will be guessed)-  -> IO FileId         -- ^ Id to pass around+-- | Open an audio file and return its ID (an opaque type that the rest of+-- library can pass around).+--+-- Throws 'IOException'.+newFile ::+  -- | Path to audio file+  FilePath ->+  -- | Type of file (or it will be guessed)+  Maybe T.FileType ->+  -- | Id to pass around+  IO FileId newFile path ftype = do   c_taglib_set_string_management_enabled $ fromBool False   ptr <- withCString path $ \cstr ->     case ftype of       Nothing -> c_taglib_file_new cstr-      Just t  -> c_taglib_file_new_type cstr (enumToCInt t)+      Just t -> c_taglib_file_new_type cstr (enumToCInt t)   when (ptr == nullPtr) $     throw (T.OpeningFailed path)   valid <- toBool <$> c_taglib_file_is_valid ptr@@ -187,28 +190,31 @@   return (FileId ptr)  -- | Free file given its ID. Every time you open a file, free it.- freeFile :: FileId -> IO () freeFile (FileId ptr) = c_taglib_file_free ptr --- | Open audio file located at specified path, execute some actions given--- its 'FileId' and then free the file.--withFile-  :: FilePath          -- ^ Path to audio file-  -> Maybe T.FileType  -- ^ Type of file (or it will be guessed)-  -> (FileId -> IO a)  -- ^ Computation depending of 'FileId'-  -> IO a              -- ^ Result value+-- | Open an audio file located at the specified path, execute some actions+-- given its 'FileId' and then free the file.+withFile ::+  -- | Path to audio file+  FilePath ->+  -- | Type of file (or it will be guessed)+  Maybe T.FileType ->+  -- | Computation depending of 'FileId'+  (FileId -> IO a) ->+  -- | Result value+  IO a withFile path t = bracket (newFile path t) freeFile --- | Save file given its ID. Given 'FilePath' just tells what to put into--- exception if the action fails, it doesn't specify where to save the file--- (it's determined by 'FileId').--saveFile-  :: FilePath          -- ^ File name to use in exceptions-  -> FileId            -- ^ File identifier-  -> IO ()+-- | Save the file given its ID. Given 'FilePath' just tells what to mention+-- in the exception messages if the action fails, it doesn't specify where+-- to save the file (it's determined by the 'FileId').+saveFile ::+  -- | File name to use in exceptions+  FilePath ->+  -- | File identifier+  FileId ->+  IO () saveFile path (FileId ptr) = do   success <- toBool <$> c_taglib_file_save ptr   unless success $@@ -217,163 +223,165 @@ ---------------------------------------------------------------------------- -- Tag API --- | Get title tag associated with file.-+-- | Get the title tag. getTitle :: FileId -> IO T.Title getTitle = fmap T.mkTitle . getStrValue c_taglib_tag_title --- | Get artist tag associated with file.-+-- | Get the artist tag. getArtist :: FileId -> IO T.Artist getArtist = fmap T.mkArtist . getStrValue c_taglib_tag_artist --- | Get album tag associated with file.-+-- | Get the album tag. getAlbum :: FileId -> IO T.Album getAlbum = fmap T.mkAlbum . getStrValue c_taglib_tag_album --- | Get comment tag associated with file.-+-- | Get the comment tag. getComment :: FileId -> IO T.Comment getComment = fmap T.mkComment . getStrValue c_taglib_tag_comment --- | Get genre tag associated with file.-+-- | Get the genre tag. getGenre :: FileId -> IO T.Genre getGenre = fmap T.mkGenre . getStrValue c_taglib_tag_genre --- | Get year tag associated with file.-+-- | Get the year tag. getYear :: FileId -> IO (Maybe T.Year) getYear = fmap T.mkYear . getIntValue c_taglib_tag_year --- | Get track number associated with file.-+-- | Get the track number. getTrackNumber :: FileId -> IO (Maybe T.TrackNumber) getTrackNumber = fmap T.mkTrackNumber . getIntValue c_taglib_tag_track --- | Set title of track associated with file.-+-- | Set the title tag. setTitle :: T.Title -> FileId -> IO () setTitle v = setStrValue c_taglib_tag_set_title (T.unTitle v) --- | Set artist of track associated with file.-+-- | Set the artist tag. setArtist :: T.Artist -> FileId -> IO () setArtist v = setStrValue c_taglib_tag_set_artist (T.unArtist v) --- | Set album of track associated with file.-+-- | Set the album tag. setAlbum :: T.Album -> FileId -> IO () setAlbum v = setStrValue c_taglib_tag_set_album (T.unAlbum v) --- | Set comment of track associated with file.-+-- | Set the comment tag. setComment :: T.Comment -> FileId -> IO () setComment v = setStrValue c_taglib_tag_set_comment (T.unComment v) --- | Set genre of track associated with file.-+-- | Set the genre tag. setGenre :: T.Genre -> FileId -> IO () setGenre v = setStrValue c_taglib_tag_set_genre (T.unGenre v) --- | Set year of track associated with file.-+-- | Set the year tag. setYear :: Maybe T.Year -> FileId -> IO () setYear v = setIntValue c_taglib_tag_set_year (T.unYear <$> v) --- | Set track number of track associated with file.-+-- | Set the track number tag. setTrackNumber :: Maybe T.TrackNumber -> FileId -> IO () setTrackNumber v = setIntValue c_taglib_tag_set_track (T.unTrackNumber <$> v)  ---------------------------------------------------------------------------- -- Audio properties API --- | Get duration of track associated with file.-+-- | Get the duration. getDuration :: FileId -> IO T.Duration-getDuration = fmap (fromJust . T.mkDuration)-  . getIntProperty c_taglib_properties_length---- | Get bit rate of track associated with file.+getDuration =+  fmap (fromJust . T.mkDuration)+    . getIntProperty c_taglib_properties_length +-- | Get the bit rate. getBitRate :: FileId -> IO T.BitRate-getBitRate = fmap (fromJust . T.mkBitRate)-  . getIntProperty c_taglib_properties_bitrate---- | Get sample rate of track associated with file.+getBitRate =+  fmap (fromJust . T.mkBitRate)+    . getIntProperty c_taglib_properties_bitrate +-- | Get the sample rate. getSampleRate :: FileId -> IO T.SampleRate-getSampleRate = fmap (fromJust . T.mkSampleRate)-  . getIntProperty c_taglib_properties_samplerate---- | Get number of channels in track associated with file.+getSampleRate =+  fmap (fromJust . T.mkSampleRate)+    . getIntProperty c_taglib_properties_samplerate +-- | Get the number of channels. getChannels :: FileId -> IO T.Channels-getChannels = fmap (fromJust . T.mkChannels)-  . getIntProperty c_taglib_properties_channels+getChannels =+  fmap (fromJust . T.mkChannels)+    . getIntProperty c_taglib_properties_channels  ---------------------------------------------------------------------------- -- Special convenience ID3v2 functions --- | Set the default encoding for ID3v2 frames that are written to tags.-+-- | Set the default encoding for ID3v2 frames. id3v2SetEncoding :: T.ID3v2Encoding -> IO () id3v2SetEncoding = c_taglib_id3v2_set_default_text_encoding . enumToCInt  ---------------------------------------------------------------------------- -- Helpers -getStrValue-  :: (Ptr TagLibTag -> IO CString) -- ^ How to get string from the resource-  -> FileId            -- ^ File ID-  -> IO Text           -- ^ String result+getStrValue ::+  -- | How to get a string from the resource+  (Ptr TagLibTag -> IO CString) ->+  -- | File ID+  FileId ->+  -- | String result+  IO Text getStrValue getStr (FileId ptr) = do-  tag    <- c_taglib_file_tag ptr-  cstr   <- getStr tag+  tag <- c_taglib_file_tag ptr+  cstr <- getStr tag   result <- packCString cstr   free cstr   return (decodeUtf8 result) -getIntValue :: Integral a-  => (Ptr TagLibTag -> IO a) -- ^ How to get value from the resource-  -> FileId            -- ^ File ID-  -> IO Int            -- ^ Result value+getIntValue ::+  (Integral a) =>+  -- | How to get a value from the resource+  (Ptr TagLibTag -> IO a) ->+  -- | File ID+  FileId ->+  -- | Result value+  IO Int getIntValue getInt (FileId ptr) = do-  tag  <- c_taglib_file_tag ptr+  tag <- c_taglib_file_tag ptr   cint <- getInt tag   return (fromIntegral cint) -setStrValue-  :: (Ptr TagLibTag -> CString -> IO ()) -- ^ Setting routine-  -> Text              -- ^ New string value-  -> FileId            -- ^ File ID-  -> IO ()+setStrValue ::+  -- | Setting routine+  (Ptr TagLibTag -> CString -> IO ()) ->+  -- | New string value+  Text ->+  -- | File ID+  FileId ->+  IO () setStrValue setStr str (FileId ptr) = do   tag <- c_taglib_file_tag ptr   useAsCString (encodeUtf8 str) $ \cstr ->     setStr tag cstr -setIntValue :: Integral a-  => (Ptr TagLibTag -> a -> IO ()) -- ^ Setting routine-  -> Maybe Int         -- ^ New value-  -> FileId            -- ^ File ID-  -> IO ()+setIntValue ::+  (Integral a) =>+  -- | Setting routine+  (Ptr TagLibTag -> a -> IO ()) ->+  -- | New value+  Maybe Int ->+  -- | File ID+  FileId ->+  IO () setIntValue setUInt int (FileId ptr) = do   tag <- c_taglib_file_tag ptr   setUInt tag (maybe 0 fromIntegral int) -getIntProperty :: Integral a-  => (Ptr TagLibProperties -> IO a) -- ^ How to get value from the resource-  -> FileId            -- ^ File ID-  -> IO Int            -- ^ Result+getIntProperty ::+  (Integral a) =>+  -- | How to get a value from the resource+  (Ptr TagLibProperties -> IO a) ->+  -- | File ID+  FileId ->+  -- | Result+  IO Int getIntProperty getInt (FileId ptr) = do   properties <- c_taglib_file_properties ptr-  value      <- getInt properties+  value <- getInt properties   return (fromIntegral value) --- | Convert Haskell enumeration to C enumeration (an integer).--enumToCInt :: Enum a => a -> CInt+-- | Convert a Haskell enumeration to a C enumeration (an integer).+enumToCInt :: (Enum a) => a -> CInt enumToCInt = fromIntegral . fromEnum
Sound/HTagLib/Setter.hs view
@@ -1,44 +1,39 @@+{-# LANGUAGE RecordWildCards #-}+ -- | -- Module      :  Sound.HTagLib.Setter--- Copyright   :  © 2015–2018 Mark Karpov+-- Copyright   :  © 2015–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com> -- Stability   :  experimental -- Portability :  portable ----- A high-level interface for writing audio meta data. You don't need to--- import this module directly, import "Sound.HTagLib" instead.--{-# LANGUAGE CPP             #-}-{-# LANGUAGE RecordWildCards #-}-+-- An interface for writing audio meta data. You don't need to import this+-- module directly, import "Sound.HTagLib" instead. module Sound.HTagLib.Setter   ( -- * High-level API-    TagSetter-  , setTags-  , setTags'+    TagSetter,+    setTags,+    setTags',+     -- * Built-in setters-  , titleSetter-  , artistSetter-  , albumSetter-  , commentSetter-  , genreSetter-  , yearSetter-  , trackNumberSetter )+    titleSetter,+    artistSetter,+    albumSetter,+    commentSetter,+    genreSetter,+    yearSetter,+    trackNumberSetter,+  ) where  import Control.Applicative ((<|>)) import Control.Monad.IO.Class import Data.Foldable (forM_)-import Data.Semigroup (Semigroup (..))+import Sound.HTagLib.Internal qualified as I import Sound.HTagLib.Type-import qualified Sound.HTagLib.Internal as I -#if !MIN_VERSION_base(4,8,0)-import Data.Monoid hiding ((<>))-#endif- -- | A composable entity that can be used together with the 'setTags' or the -- 'setTags'' functions to write meta data to an audio file. --@@ -47,113 +42,121 @@ -- > titleSetter "foo" <> titleSetter "bar" -- -- The first value wins.- data TagSetter = TagSetter-  { sdTitle       :: Maybe Title-  , sdArtist      :: Maybe Artist-  , sdAlbum       :: Maybe Album-  , sdComment     :: Maybe Comment-  , sdGenre       :: Maybe Genre-  , sdYear        :: Maybe (Maybe Year)-  , sdTrackNumber :: Maybe (Maybe TrackNumber) }+  { sdTitle :: Maybe Title,+    sdArtist :: Maybe Artist,+    sdAlbum :: Maybe Album,+    sdComment :: Maybe Comment,+    sdGenre :: Maybe Genre,+    sdYear :: Maybe (Maybe Year),+    sdTrackNumber :: Maybe (Maybe TrackNumber)+  }  -- | @since 1.2.0- instance Semigroup TagSetter where-  x <> y = let f g = g x <|> g y in TagSetter-    { sdTitle       = f sdTitle-    , sdArtist      = f sdArtist-    , sdAlbum       = f sdAlbum-    , sdComment     = f sdComment-    , sdGenre       = f sdGenre-    , sdYear        = f sdYear-    , sdTrackNumber = f sdTrackNumber }+  x <> y =+    let f g = g x <|> g y+     in TagSetter+          { sdTitle = f sdTitle,+            sdArtist = f sdArtist,+            sdAlbum = f sdAlbum,+            sdComment = f sdComment,+            sdGenre = f sdGenre,+            sdYear = f sdYear,+            sdTrackNumber = f sdTrackNumber+          }  instance Monoid TagSetter where-  mempty = TagSetter-    { sdTitle       = Nothing-    , sdArtist      = Nothing-    , sdAlbum       = Nothing-    , sdComment     = Nothing-    , sdGenre       = Nothing-    , sdYear        = Nothing-    , sdTrackNumber = Nothing }+  mempty =+    TagSetter+      { sdTitle = Nothing,+        sdArtist = Nothing,+        sdAlbum = Nothing,+        sdComment = Nothing,+        sdGenre = Nothing,+        sdYear = Nothing,+        sdTrackNumber = Nothing+      }   mappend = (<>) --- | Set tags in specified file using the given setter.+-- | Set tags in a specified file using the given setter. ----- In the case of trouble 'I.HTagLibException' will be thrown.--setTags :: MonadIO m-  => FilePath          -- ^ Path to audio file-  -> Maybe ID3v2Encoding -- ^ Encoding for ID3v2 frames-  -> TagSetter         -- ^ Setter-  -> m ()+-- Throws 'I.HTagLibException'.+setTags ::+  (MonadIO m) =>+  -- | Path to audio file+  FilePath ->+  -- | Encoding for ID3v2 frames+  Maybe ID3v2Encoding ->+  -- | Setter+  TagSetter ->+  m () setTags path enc = execSetter path enc Nothing --- | Similar to 'setTags', but you can also specify type of audio file--- explicitly (otherwise it's guessed from file extension).--setTags' :: MonadIO m-  => FilePath          -- ^ Path to audio file-  -> Maybe ID3v2Encoding -- ^ Encoding for ID3v2 frames-  -> FileType          -- ^ Type of audio file-  -> TagSetter         -- ^ Setter-  -> m ()+-- | Similar to 'setTags', but you can also specify the type of the audio+-- file explicitly (otherwise it's guessed from the file extension).+setTags' ::+  (MonadIO m) =>+  -- | Path to audio file+  FilePath ->+  -- | Encoding for ID3v2 frames+  Maybe ID3v2Encoding ->+  -- | Type of audio file+  FileType ->+  -- | Setter+  TagSetter ->+  m () setTags' path enc t = execSetter path enc (Just t)  -- | The most general way to set meta data. 'setTags' and 'setTags'' are -- just wrappers around this function.--execSetter :: MonadIO m-  => FilePath          -- ^ Path to audio file-  -> Maybe ID3v2Encoding -- ^ Encoding for ID3v2 frames-  -> Maybe FileType    -- ^ Type of audio file (if known)-  -> TagSetter         -- ^ Setter-  -> m ()+execSetter ::+  (MonadIO m) =>+  -- | Path to audio file+  FilePath ->+  -- | Encoding for ID3v2 frames+  Maybe ID3v2Encoding ->+  -- | Type of audio file (if known)+  Maybe FileType ->+  -- | Setter+  TagSetter ->+  m () execSetter path enc t TagSetter {..} = liftIO . I.withFile path t $ \fid -> do   forM_ enc I.id3v2SetEncoding   let writeTag x f = forM_ x (`f` fid)-  writeTag sdTitle       I.setTitle-  writeTag sdArtist      I.setArtist-  writeTag sdAlbum       I.setAlbum-  writeTag sdComment     I.setComment-  writeTag sdGenre       I.setGenre-  writeTag sdYear        I.setYear+  writeTag sdTitle I.setTitle+  writeTag sdArtist I.setArtist+  writeTag sdAlbum I.setAlbum+  writeTag sdComment I.setComment+  writeTag sdGenre I.setGenre+  writeTag sdYear I.setYear   writeTag sdTrackNumber I.setTrackNumber   I.saveFile path fid --- | Setter for track title.-+-- | Setter for the track title. titleSetter :: Title -> TagSetter-titleSetter x = mempty { sdTitle = Just x }---- | Setter for track artist.+titleSetter x = mempty {sdTitle = Just x} +-- | Setter for the track artist. artistSetter :: Artist -> TagSetter-artistSetter x = mempty { sdArtist = Just x }---- | Setter for track album.+artistSetter x = mempty {sdArtist = Just x} +-- | Setter for the track album. albumSetter :: Album -> TagSetter-albumSetter x = mempty { sdAlbum = Just x }---- | Setter for track comment.+albumSetter x = mempty {sdAlbum = Just x} +-- | Setter for the track comment. commentSetter :: Comment -> TagSetter-commentSetter x = mempty { sdComment = Just x }---- | Setter for track genre.+commentSetter x = mempty {sdComment = Just x} +-- | Setter for the track genre. genreSetter :: Genre -> TagSetter-genreSetter x = mempty { sdGenre = Just x }---- | Setter for year tag, use 'Nothing' to clear the field.+genreSetter x = mempty {sdGenre = Just x} +-- | Setter for the year tag, use 'Nothing' to clear the field. yearSetter :: Maybe Year -> TagSetter-yearSetter x = mempty { sdYear = Just x }---- | Setter for track number, use 'Nothing' to clear the field.+yearSetter x = mempty {sdYear = Just x} +-- | Setter for the track number, use 'Nothing' to clear the field. trackNumberSetter :: Maybe TrackNumber -> TagSetter-trackNumberSetter x = mempty { sdTrackNumber = Just x }+trackNumberSetter x = mempty {sdTrackNumber = Just x}
Sound/HTagLib/Type.hs view
@@ -1,6 +1,9 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module      :  Sound.HTagLib.Type--- Copyright   :  © 2015–2018 Mark Karpov+-- Copyright   :  © 2015–present Mark Karpov -- License     :  BSD 3 clause -- -- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>@@ -8,275 +11,247 @@ -- Portability :  portable -- -- Definitions of types used to represent various tags and audio properties.--{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE OverloadedStrings  #-}- module Sound.HTagLib.Type-  ( Title-  , mkTitle-  , unTitle-  , Artist-  , mkArtist-  , unArtist-  , Album-  , mkAlbum-  , unAlbum-  , Comment-  , mkComment-  , unComment-  , Genre-  , mkGenre-  , unGenre-  , Year-  , mkYear-  , unYear-  , TrackNumber-  , mkTrackNumber-  , unTrackNumber-  , Duration-  , mkDuration-  , unDuration-  , BitRate-  , mkBitRate-  , unBitRate-  , SampleRate-  , mkSampleRate-  , unSampleRate-  , Channels-  , mkChannels-  , unChannels-  , FileType (..)-  , ID3v2Encoding (..)-  , HTagLibException (..) )+  ( Title,+    mkTitle,+    unTitle,+    Artist,+    mkArtist,+    unArtist,+    Album,+    mkAlbum,+    unAlbum,+    Comment,+    mkComment,+    unComment,+    Genre,+    mkGenre,+    unGenre,+    Year,+    mkYear,+    unYear,+    TrackNumber,+    mkTrackNumber,+    unTrackNumber,+    Duration,+    mkDuration,+    unDuration,+    BitRate,+    mkBitRate,+    unBitRate,+    SampleRate,+    mkSampleRate,+    unSampleRate,+    Channels,+    mkChannels,+    unChannels,+    FileType (..),+    ID3v2Encoding (..),+    HTagLibException (..),+  ) where  import Control.Exception (Exception) import Data.String import Data.Text (Text)+import Data.Text qualified as T import Data.Typeable (Typeable)-import qualified Data.Text as T  -- | Title tag.- newtype Title = Title Text deriving (Show, Eq, Ord)  instance IsString Title where   fromString = mkTitle . fromString --- | Construction of 'Title' type, null bytes are converted to spaces.-+-- | Construction of 'Title' values, null bytes are converted to spaces. mkTitle :: Text -> Title mkTitle = Title . avoidNulls  -- | Convert 'Title' to 'Text'.- unTitle :: Title -> Text unTitle (Title x) = x  -- | Artist tag.- newtype Artist = Artist Text deriving (Show, Eq, Ord)  instance IsString Artist where   fromString = mkArtist . fromString --- | Construction of 'Artist' type, null bytes are converted to spaces.-+-- | Construction of 'Artist' values, null bytes are converted to spaces. mkArtist :: Text -> Artist mkArtist = Artist . avoidNulls  -- | Convert 'Artist' to 'Text'.- unArtist :: Artist -> Text unArtist (Artist x) = x  -- | Album tag.- newtype Album = Album Text deriving (Show, Eq, Ord)  instance IsString Album where   fromString = mkAlbum . fromString --- | Construction of 'Album' type, null bytes are converted to spaces.-+-- | Construction of 'Album' values, null bytes are converted to spaces. mkAlbum :: Text -> Album mkAlbum = Album . avoidNulls  -- | Convert 'Album' to 'Text'.- unAlbum :: Album -> Text unAlbum (Album x) = x  -- | Comment tag.- newtype Comment = Comment Text deriving (Show, Eq, Ord)  instance IsString Comment where   fromString = mkComment . fromString --- | Construction of 'Comment' type, null bytes are converted to spaces.-+-- | Construction of 'Comment' values, null bytes are converted to spaces. mkComment :: Text -> Comment mkComment = Comment . avoidNulls  -- | Convert 'Comment' to 'Text'.- unComment :: Comment -> Text unComment (Comment x) = x  -- | Genre tag.- newtype Genre = Genre Text deriving (Show, Eq, Ord)  instance IsString Genre where   fromString = mkGenre . fromString --- | Construction of 'Genre' type, null bytes are converted to spaces.-+-- | Construction of 'Genre' values, null bytes are converted to spaces. mkGenre :: Text -> Genre mkGenre = Genre . avoidNulls  -- | Convert 'Genre' to 'Text'.- unGenre :: Genre -> Text unGenre (Genre x) = x  -- | Year tag.- newtype Year = Year Int deriving (Show, Eq, Ord) --- | Construction of 'Year' type, non-positive values result in 'Nothing'.-+-- | Construction of 'Year' values, non-positive values result in 'Nothing'. mkYear :: Int -> Maybe Year mkYear = fmap Year . atLeast 1  -- | Convert 'Year' to 'Int'.- unYear :: Year -> Int unYear (Year x) = x  -- | Track number tag.- newtype TrackNumber = TrackNumber Int deriving (Show, Eq, Ord) --- | Construction of 'TrackNumber' type, non-positive values result in+-- | Construction of 'TrackNumber' values, non-positive values result in -- 'Nothing'.- mkTrackNumber :: Int -> Maybe TrackNumber mkTrackNumber = fmap TrackNumber . atLeast 1  -- | Convert 'TrackNumber' to 'Int'.- unTrackNumber :: TrackNumber -> Int unTrackNumber (TrackNumber x) = x  -- | Duration in seconds.- newtype Duration = Duration Int deriving (Show, Eq, Ord)  -- | Construction of 'Duration' values, negative values result in 'Nothing'.- mkDuration :: Int -> Maybe Duration mkDuration = fmap Duration . atLeast 0  -- | Convert 'Duration' to 'Int'.- unDuration :: Duration -> Int unDuration (Duration x) = x  -- | Bit rate in kb/s.- newtype BitRate = BitRate Int deriving (Show, Eq, Ord) --- | Construction of 'BitRate' values, negative values result in--- 'Nothing'.-+-- | Construction of 'BitRate' values, negative values result in 'Nothing'. mkBitRate :: Int -> Maybe BitRate mkBitRate = fmap BitRate . atLeast 0  -- | Convert 'BitRate' to 'Int'.- unBitRate :: BitRate -> Int unBitRate (BitRate x) = x  -- | Sample rate in Hz.- newtype SampleRate = SampleRate Int deriving (Show, Eq, Ord)  -- | Construction of 'SampleRate' values, non-positive values result in -- 'Nothing'.- mkSampleRate :: Int -> Maybe SampleRate mkSampleRate = fmap SampleRate . atLeast 1  -- | Convert 'SampleRate' to 'Int'.- unSampleRate :: SampleRate -> Int unSampleRate (SampleRate x) = x  -- | Number of channels in the audio stream.- newtype Channels = Channels Int deriving (Show, Eq, Ord)  -- | Construction of 'Channels' values, non-positive values result in -- 'Nothing'.- mkChannels :: Int -> Maybe Channels mkChannels = fmap Channels . atLeast 1  -- | Convert 'Channels' to 'Int'.- unChannels :: Channels -> Int unChannels (Channels x) = x  -- | Replace null bytes with spaces.- avoidNulls :: Text -> Text avoidNulls = T.replace "\0" " "  -- | @atLeast a b@ returns @Just b@ is @b@ is greater or equal to @a@,--- otherwise result is @Nothing@.-+-- otherwise the result is @Nothing@. atLeast :: Int -> Int -> Maybe Int atLeast a b = if b >= a then Just b else Nothing --- | Types of files TagLib can work with. This may be used to explicitly--- specify type of file instead of relying on the TagLib's ability to guess--- type of file from its extension.-+-- | The types of files TagLib can work with. This may be used to explicitly+-- specify the type of a file instead of relying on the TagLib's ability to+-- guess the type of a file from its extension. data FileType-  = MPEG               -- ^ MPEG-  | OggVorbis          -- ^ Ogg vorbis-  | FLAC               -- ^ FLAC-  | MPC                -- ^ MPC-  | OggFlac            -- ^ Ogg FLAC-  | WavPack            -- ^ Wav pack-  | Speex              -- ^ Speex-  | TrueAudio          -- ^ True audio-  | MP4                -- ^ MP4-  | ASF                -- ^ ASF-    deriving (Show, Eq, Enum)+  = -- | MPEG+    MPEG+  | -- | Ogg vorbis+    OggVorbis+  | -- | FLAC+    FLAC+  | -- | MPC+    MPC+  | -- | Ogg FLAC+    OggFlac+  | -- | Wav pack+    WavPack+  | -- | Speex+    Speex+  | -- | True audio+    TrueAudio+  | -- | MP4+    MP4+  | -- | ASF+    ASF+  deriving (Show, Eq, Enum)  -- | Encoding for ID3v2 frames that are written to tags.- data ID3v2Encoding-  = ID3v2Latin1        -- ^ Latin1-  | ID3v2UTF16         -- ^ UTF-16-  | ID3v2UTF16BE       -- ^ UTF-16 big endian-  | ID3v2UTF8          -- ^ UTF-8-    deriving (Show, Eq, Enum)+  = -- | Latin1+    ID3v2Latin1+  | -- | UTF-16+    ID3v2UTF16+  | -- | UTF-16 big endian+    ID3v2UTF16BE+  | -- | UTF-8+    ID3v2UTF8+  deriving (Show, Eq, Enum)  -- | The data type represents exceptions specific to the library.- data HTagLibException-  = OpeningFailed FilePath-    -- ^ Attempt to open audio file to read its tags failed-  | InvalidFile FilePath-    -- ^ File can be opened, but it doesn't contain any information that can+  = -- | An attempt to open an audio file to read its tags failed+    OpeningFailed FilePath+  | -- | The file can be opened, but it doesn't contain any information that can     -- be interpreted by the library-  | SavingFailed FilePath-    -- ^ Saving failed-    deriving (Eq, Show, Typeable)+    InvalidFile FilePath+  | -- | Saving failed+    SavingFailed FilePath+  deriving (Eq, Show, Typeable)  instance Exception HTagLibException
htaglib.cabal view
@@ -1,72 +1,80 @@-name:                 htaglib-version:              1.2.0-cabal-version:        1.18-tested-with:          GHC==7.8.4, GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.1-license:              BSD3-license-file:         LICENSE.md-author:               Mark Karpov <markkarpov92@gmail.com>-maintainer:           Mark Karpov <markkarpov92@gmail.com>-homepage:             https://github.com/mrkkrp/htaglib-bug-reports:          https://github.com/mrkkrp/htaglib/issues-category:             Sound, Foreign-synopsis:             Bindings to TagLib, audio meta-data library-build-type:           Simple-description:          Bindings to TagLib, audio meta-data library.-extra-doc-files:      CHANGELOG.md-                    , README.md-                    , audio-samples/README.md-data-files:           audio-samples/*.flac-                    , audio-samples/*.mp3+cabal-version:   2.4+name:            htaglib+version:         1.2.1+license:         BSD-3-Clause+license-file:    LICENSE.md+maintainer:      Mark Karpov <markkarpov92@gmail.com>+author:          Mark Karpov <markkarpov92@gmail.com>+tested-with:     ghc ==9.4.7 ghc ==9.6.3 ghc ==9.8.1+homepage:        https://github.com/mrkkrp/htaglib+bug-reports:     https://github.com/mrkkrp/htaglib/issues+synopsis:        Bindings to TagLib, audio meta-data library+description:     Bindings to TagLib, audio meta-data library.+category:        Sound, Foreign+build-type:      Simple+data-files:+    audio-samples/*.flac+    audio-samples/*.mp3 +extra-doc-files:+    CHANGELOG.md+    README.md+    audio-samples/README.md+ source-repository head-  type:               git-  location:           https://github.com/mrkkrp/htaglib.git+    type:     git+    location: https://github.com/mrkkrp/htaglib.git  flag dev-  description:        Turn on development settings.-  manual:             True-  default:            False+    description: Turn on development settings.+    default:     False+    manual:      True  library-  build-depends:      base         >= 4.7 && < 5.0-                    , bytestring   >= 0.9 && < 0.11-                    , text         >= 1.0 && < 1.3-                    , transformers >= 0.4 && < 0.6-  if !impl(ghc >= 8.0)-    build-depends:    semigroups       == 0.18.*-  extra-libraries:    tag_c-  exposed-modules:    Sound.HTagLib-  other-modules:      Sound.HTagLib.Type-                    , Sound.HTagLib.Getter-                    , Sound.HTagLib.Setter-                    , Sound.HTagLib.Internal-  if flag(dev)-    ghc-options:      -Wall -Werror-  else-    ghc-options:      -O2 -Wall-  if flag(dev) && impl(ghc >= 8.0)-    ghc-options:      -Wcompat-                      -Wincomplete-record-updates-                      -Wincomplete-uni-patterns-                      -Wnoncanonical-monad-instances-                      -Wnoncanonical-monadfail-instances-  default-language:   Haskell2010+    exposed-modules:  Sound.HTagLib+    other-modules:+        Sound.HTagLib.Type+        Sound.HTagLib.Getter+        Sound.HTagLib.Setter+        Sound.HTagLib.Internal +    default-language: GHC2021+    extra-libraries:  tag_c+    build-depends:+        base >=4.15 && <5,+        bytestring >=0.9 && <0.12,+        text >=1 && <2.2++    if flag(dev)+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages++    else+        ghc-options: -O2 -Wall+ test-suite tests-  main-is:            Spec.hs-  hs-source-dirs:     tests-  type:               exitcode-stdio-1.0-  if flag(dev)-    ghc-options:      -Wall -Werror-  else-    ghc-options:      -O2 -Wall-  other-modules:      Sound.HTagLib.GetterSpec-                    , Sound.HTagLib.SetterSpec-                    , Sound.HTagLib.Test.Util-  build-depends:      base       >= 4.7 && < 5.0-                    , directory  >= 1.2 && < 1.4-                    , filepath   >= 1.4 && < 2.0-                    , hspec      >= 2.0 && < 3.0-                    , htaglib-  build-tools:        hspec-discover >= 2.0 && < 3.0-  default-language:   Haskell2010+    type:               exitcode-stdio-1.0+    main-is:            Spec.hs+    build-tool-depends: hspec-discover:hspec-discover >=2 && <3+    hs-source-dirs:     tests+    other-modules:+        Sound.HTagLib.GetterSpec+        Sound.HTagLib.SetterSpec+        Sound.HTagLib.Test.Util++    default-language:   GHC2021+    build-depends:+        base >=4.15 && <5,+        directory >=1.2 && <1.4,+        filepath >=1.4 && <2,+        hspec >=2 && <3,+        htaglib++    if flag(dev)+        ghc-options:+            -Wall -Werror -Wredundant-constraints -Wpartial-fields+            -Wunused-packages++    else+        ghc-options: -O2 -Wall
tests/Sound/HTagLib/GetterSpec.hs view
@@ -1,20 +1,14 @@-{-# LANGUAGE CPP #-}- module Sound.HTagLib.GetterSpec (spec) where  import Sound.HTagLib import Sound.HTagLib.Test.Util import Test.Hspec -#if !MIN_VERSION_base(4,8,0)-import Control.Applicative-#endif- spec :: Spec spec =   describe "getters" $ do     mapM_ (withFile $ const simpleGetter) fileList-    mapM_ (withFile specializedGetter)    fileList+    mapM_ (withFile specializedGetter) fileList  simpleGetter :: AudioTags -> Expectation simpleGetter tags = do
tests/Sound/HTagLib/SetterSpec.hs view
@@ -1,23 +1,18 @@-{-# LANGUAGE CPP               #-} {-# LANGUAGE OverloadedStrings #-}  module Sound.HTagLib.SetterSpec (spec) where  import Sound.HTagLib import Sound.HTagLib.Test.Util-import System.Directory (getTemporaryDirectory, copyFile)-import System.FilePath ((</>), takeFileName)+import System.Directory (copyFile, getTemporaryDirectory)+import System.FilePath (takeFileName, (</>)) import Test.Hspec -#if !MIN_VERSION_base(4,8,0)-import Control.Applicative-#endif- spec :: Spec spec =   describe "setters" $ do     mapM_ (withFile $ const simpleSetter) fileList-    mapM_ (withFile specializedSetter)    fileList+    mapM_ (withFile specializedSetter) fileList  dupeFile :: FilePath -> IO FilePath dupeFile path = do@@ -26,14 +21,16 @@   return newPath  updateSampleTags :: AudioTags -> AudioTags-updateSampleTags tags = tags-  { atTitle       = mkTitle "title'"-  , atArtist      = mkArtist "artist'"-  , atAlbum       = mkAlbum "album'"-  , atComment     = mkComment "comment'"-  , atGenre       = mkGenre "genre'"-  , atYear        = mkYear 2056-  , atTrackNumber = mkTrackNumber 8 }+updateSampleTags tags =+  tags+    { atTitle = mkTitle "title'",+      atArtist = mkArtist "artist'",+      atAlbum = mkAlbum "album'",+      atComment = mkComment "comment'",+      atGenre = mkGenre "genre'",+      atYear = mkYear 2056,+      atTrackNumber = mkTrackNumber 8+    }  simpleSetter :: AudioTags -> Expectation simpleSetter tags = do@@ -41,7 +38,7 @@   dupe <- dupeFile path   setTags dupe Nothing sampleSetter   extracted <- getTags dupe (sampleGetter dupe)-  extracted `shouldMatchTags` updateSampleTags (tags { atFileName = dupe })+  extracted `shouldMatchTags` updateSampleTags (tags {atFileName = dupe})  specializedSetter :: FileType -> AudioTags -> Expectation specializedSetter t tags = do@@ -49,4 +46,4 @@   dupe <- dupeFile path   setTags' dupe Nothing t sampleSetter   extracted <- getTags dupe (sampleGetter dupe)-  extracted `shouldMatchTags` updateSampleTags (tags { atFileName = dupe })+  extracted `shouldMatchTags` updateSampleTags (tags {atFileName = dupe})
tests/Sound/HTagLib/Test/Util.hs view
@@ -1,111 +1,116 @@-{-# LANGUAGE CPP               #-} {-# LANGUAGE OverloadedStrings #-}  module Sound.HTagLib.Test.Util-  ( AudioTags (..)-  , sampleGetter-  , sampleSetter-  , fileList-  , withFile-  , shouldMatchTags )+  ( AudioTags (..),+    sampleGetter,+    sampleSetter,+    fileList,+    withFile,+    shouldMatchTags,+  ) where  import Data.Maybe (fromJust) import Sound.HTagLib import Test.Hspec -#if !MIN_VERSION_base(4,8,0)-import Control.Applicative-#endif-#if !MIN_VERSION_base(4,11,0)-import Data.Monoid-#endif- data AudioTags = AudioTags-  { atFileName    :: FilePath-  , atTitle       :: Title-  , atArtist      :: Artist-  , atAlbum       :: Album-  , atComment     :: Comment-  , atGenre       :: Genre-  , atYear        :: Maybe Year-  , atTrackNumber :: Maybe TrackNumber-  , atDuration    :: Duration-  , atBitRate     :: BitRate-  , atSampleRate  :: SampleRate-  , atChannels    :: Channels }+  { atFileName :: FilePath,+    atTitle :: Title,+    atArtist :: Artist,+    atAlbum :: Album,+    atComment :: Comment,+    atGenre :: Genre,+    atYear :: Maybe Year,+    atTrackNumber :: Maybe TrackNumber,+    atDuration :: Duration,+    atBitRate :: BitRate,+    atSampleRate :: SampleRate,+    atChannels :: Channels+  }   deriving (Show, Eq)  sampleGetter :: FilePath -> TagGetter AudioTags-sampleGetter path = AudioTags-  <$> pure path-  <*> titleGetter-  <*> artistGetter-  <*> albumGetter-  <*> commentGetter-  <*> genreGetter-  <*> yearGetter-  <*> trackNumberGetter-  <*> durationGetter-  <*> bitRateGetter-  <*> sampleRateGetter-  <*> channelsGetter+sampleGetter path =+  AudioTags+    <$> pure path+    <*> titleGetter+    <*> artistGetter+    <*> albumGetter+    <*> commentGetter+    <*> genreGetter+    <*> yearGetter+    <*> trackNumberGetter+    <*> durationGetter+    <*> bitRateGetter+    <*> sampleRateGetter+    <*> channelsGetter  sampleSetter :: TagSetter sampleSetter =-  mempty <>-  titleSetter (mkTitle "title'") <>-  artistSetter (mkArtist "artist'") <>-  albumSetter (mkAlbum "album'") <>-  commentSetter (mkComment "comment'") <>-  genreSetter (mkGenre "genre'") <>-  yearSetter (mkYear 2056) <>-  trackNumberSetter (mkTrackNumber 8)+  mempty+    <> titleSetter (mkTitle "title'")+    <> artistSetter (mkArtist "artist'")+    <> albumSetter (mkAlbum "album'")+    <> commentSetter (mkComment "comment'")+    <> genreSetter (mkGenre "genre'")+    <> yearSetter (mkYear 2056)+    <> trackNumberSetter (mkTrackNumber 8)  sampleTags :: AudioTags-sampleTags = AudioTags-  { atFileName    = undefined-  , atTitle       = "title"-  , atArtist      = "artist"-  , atAlbum       = "album"-  , atComment     = "comment"-  , atGenre       = "genre"-  , atYear        = mkYear 2055-  , atTrackNumber = mkTrackNumber 7-  , atDuration    = fromJust $ mkDuration 0-  , atBitRate     = fromJust $ mkBitRate 0-  , atSampleRate  = fromJust $ mkSampleRate 44100-  , atChannels    = fromJust $ mkChannels 2 }+sampleTags =+  AudioTags+    { atFileName = undefined,+      atTitle = "title",+      atArtist = "artist",+      atAlbum = "album",+      atComment = "comment",+      atGenre = "genre",+      atYear = mkYear 2055,+      atTrackNumber = mkTrackNumber 7,+      atDuration = fromJust $ mkDuration 0,+      atBitRate = fromJust $ mkBitRate 0,+      atSampleRate = fromJust $ mkSampleRate 44100,+      atChannels = fromJust $ mkChannels 2+    }  fileList :: [(FileType, AudioTags)] fileList =-  [ (FLAC, sampleTags-     { atBitRate = fromJust $ mkBitRate 217-     , atFileName = "audio-samples/sample.flac" })-  , (MPEG, sampleTags-     { atBitRate = fromJust $ mkBitRate 136-     , atFileName = "audio-samples/sample.mp3"  }) ]---- | Call given function with provided data.+  [ ( FLAC,+      sampleTags+        { atBitRate = fromJust $ mkBitRate 217,+          atFileName = "audio-samples/sample.flac"+        }+    ),+    ( MPEG,+      sampleTags+        { atBitRate = fromJust $ mkBitRate 136,+          atFileName = "audio-samples/sample.mp3"+        }+    )+  ] -withFile-  :: (FileType -> AudioTags -> Expectation)-  -> (FileType, AudioTags)-  -> Spec+-- | Call a given function with the provided data.+withFile ::+  (FileType -> AudioTags -> Expectation) ->+  (FileType, AudioTags) ->+  Spec withFile f (t, tags) = it name (f t tags)-  where name = "using file: " ++ show (atFileName tags) ++ " (" ++ show t ++ ")"---- | Create an expectation that two collections of tags match. However, if--- bit rate of the first is zero (which is the case with older versions of--- TagLib when it's used with such short files as our samples), allow bit--- rate values differ.+  where+    name = "using file: " ++ show (atFileName tags) ++ " (" ++ show t ++ ")" -shouldMatchTags-  :: AudioTags         -- ^ Tags to test-  -> AudioTags         -- ^ Correct tags to test against-  -> Expectation+-- | Create an expectation that the two collections of tags match. However,+-- if the bit rate in the first collection is zero (which is the case with+-- the older versions of TagLib when it's used with such short files as our+-- samples), allow bit rate values to differ.+shouldMatchTags ::+  -- | Tags to test+  AudioTags ->+  -- | The value to test against+  AudioTags ->+  Expectation shouldMatchTags given expected =   let zeroBitRate = fromJust (mkBitRate 0)-  in if atBitRate given == zeroBitRate-       then given `shouldBe` expected { atBitRate = zeroBitRate }-       else given `shouldBe` expected+   in if atBitRate given == zeroBitRate+        then given `shouldBe` expected {atBitRate = zeroBitRate}+        else given `shouldBe` expected