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 +4/−0
- LICENSE.md +1/−1
- README.md +62/−79
- Setup.hs +0/−6
- Sound/HTagLib.hs +66/−64
- Sound/HTagLib/Getter.hs +77/−83
- Sound/HTagLib/Internal.hs +146/−138
- Sound/HTagLib/Setter.hs +102/−99
- Sound/HTagLib/Type.hs +91/−116
- htaglib.cabal +72/−64
- tests/Sound/HTagLib/GetterSpec.hs +1/−7
- tests/Sound/HTagLib/SetterSpec.hs +15/−18
- tests/Sound/HTagLib/Test/Util.hs +90/−85
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 @@ [](https://hackage.haskell.org/package/htaglib) [](http://stackage.org/nightly/package/htaglib) [](http://stackage.org/lts/package/htaglib)-[](https://travis-ci.org/mrkkrp/htaglib)+ * [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