haskell-ffprobe (empty) → 0.1.0.0
raw patch · 19 files changed
+730/−0 lines, 19 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, haskell-ffprobe, hspec, hspec-expectations, process, scientific, text
Files
- CHANGELOG.md +11/−0
- LICENSE +26/−0
- README.md +24/−0
- Setup.hs +2/−0
- haskell-ffprobe.cabal +88/−0
- src/FFProbe.hs +29/−0
- src/FFProbe/Data/Chapter.hs +46/−0
- src/FFProbe/Data/Format.hs +45/−0
- src/FFProbe/Data/Stream.hs +181/−0
- src/FFProbe/Data/Tags.hs +29/−0
- src/FFProbe/Data/Tags/Internal.hs +16/−0
- src/FFProbe/Exec.hs +24/−0
- src/FFProbe/Internal.hs +15/−0
- test/FFProbe/Data/TestChapter.hs +22/−0
- test/FFProbe/Data/TestFormat.hs +28/−0
- test/FFProbe/Data/TestStream.hs +94/−0
- test/FFProbe/TestFFProbe.hs +27/−0
- test/Spec.hs +12/−0
- test/Utils.hs +11/−0
+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `haskell-ffprobe`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 0.1.0.0 - 2024-07-01++First release!
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2024 Author name here++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its contributors+ may be used to endorse or promote products derived from this software+ without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,24 @@+# haskell-ffprobe++This package provides Haskell bindings for the `ffprobe` command.++## Example++```haskell+import FFProbe+import FFProbe.Data.Format (duration, formatName)+import FFProbe.Data.Stream (codecLongName)+import System.Environment++main :: IO ()+main = do+ fileName:_ <- getArgs+ ffprobeRes <- ffprobe fileName+ case ffprobeRes of+ Left err -> putStrLn $ "An error occured: " ++ err+ Right ffprobeData -> do+ print $ formatName (format ffprobeData)+ print $ duration (format ffprobeData)+ print $ length (chapters ffprobeData)+ print $ codecLongName $ head (streams ffprobeData)+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ haskell-ffprobe.cabal view
@@ -0,0 +1,88 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.36.0.+--+-- see: https://github.com/sol/hpack++name: haskell-ffprobe+version: 0.1.0.0+synopsis: Haskell bindings for ffprobe+description: Please see the README on GitHub at <https://github.com/Arthi-chaud/haskell-ffprobe#readme>+category: Bindings+homepage: https://github.com/Arthi-chaud/haskell-ffprobe#readme+bug-reports: https://github.com/Arthi-chaud/haskell-ffprobe/issues+author: Arthi-chaud+maintainer: Arthi-chaud+copyright: 2024 Arthi-chaud+license: BSD-3-Clause+license-file: LICENSE+build-type: Simple+extra-doc-files:+ README.md+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/Arthi-chaud/haskell-ffprobe++library+ exposed-modules:+ FFProbe+ FFProbe.Data.Chapter+ FFProbe.Data.Format+ FFProbe.Data.Stream+ FFProbe.Data.Tags+ other-modules:+ FFProbe.Data.Tags.Internal+ FFProbe.Exec+ FFProbe.Internal+ Paths_haskell_ffprobe+ autogen-modules:+ Paths_haskell_ffprobe+ hs-source-dirs:+ src+ default-extensions:+ OverloadedStrings+ LambdaCase+ FlexibleInstances+ RecordWildCards+ DeriveGeneric+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+ build-depends:+ aeson >=2.1.2.1 && <=2.2.3.0+ , base >=4.7 && <5+ , bytestring >=0.11.5 && <0.12+ , process >=1.6.18 && <1.7+ , scientific >=0.3.7 && <=0.3.8.0+ , text >=2.0.2 && <2.1+ default-language: Haskell2010++test-suite haskell-ffprobe-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ FFProbe.Data.TestChapter+ FFProbe.Data.TestFormat+ FFProbe.Data.TestStream+ FFProbe.TestFFProbe+ Utils+ Paths_haskell_ffprobe+ autogen-modules:+ Paths_haskell_ffprobe+ hs-source-dirs:+ test+ default-extensions:+ OverloadedStrings+ LambdaCase+ FlexibleInstances+ RecordWildCards+ DeriveGeneric+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ aeson >=2.1.2.1 && <=2.2.3.0+ , base >=4.7 && <5+ , bytestring >=0.11.5 && <0.12+ , haskell-ffprobe+ , hspec+ , hspec-expectations+ default-language: Haskell2010
+ src/FFProbe.hs view
@@ -0,0 +1,29 @@+module FFProbe (+ ffprobe,+ FFProbeData (..),+) where++import Data.Aeson (FromJSON, eitherDecodeStrict)+import Data.ByteString.Char8 (pack)+import FFProbe.Data.Chapter (Chapter)+import FFProbe.Data.Format (Format)+import FFProbe.Data.Stream (Stream)+import FFProbe.Exec (execFFProbe)+import GHC.Generics (Generic)++data FFProbeData = FFProbeData+ { streams :: [Stream],+ chapters :: [Chapter],+ format :: Format+ }+ deriving (Generic)++instance FromJSON FFProbeData++-- | Runs the ffprobe coomands, and parse its output+ffprobe :: String -> IO (Either String FFProbeData)+ffprobe path = do+ probeRes <- execFFProbe path+ return $ case probeRes of+ Right rawJson -> eitherDecodeStrict (pack rawJson)+ Left err -> Left err
+ src/FFProbe/Data/Chapter.hs view
@@ -0,0 +1,46 @@+module FFProbe.Data.Chapter (Chapter (..), duration, title) where++import Data.Aeson+import FFProbe.Data.Tags+import FFProbe.Data.Tags.Internal+import FFProbe.Internal (parseReadable)+import Prelude hiding (id)++data Chapter = Chapter+ { id :: Integer,+ -- | Example: "1/1000000000"+ timeBase :: String,+ -- | The timestamp, in seconds, of the start of the chapter+ startTime :: Float,+ -- | The timestamp, in seconds, of the end of the chapter+ endTime :: Float,+ -- | Additional tags+ tags :: TagList,+ -- | The aeson object for the entire JSON received from ffprobe.+ raw :: Object+ }++instance HasTags Chapter where+ getTags = tags++-- | Gets the duration of the chapter, in seconds+duration :: Chapter -> Float+duration chapter = endTime chapter - startTime chapter++-- | Retrieves the title of the chapter, using its tags+title :: Chapter -> Maybe String+title chapter = do+ value <- lookupTag "title" chapter+ case value of+ StringTag t -> return t+ _ -> Nothing++instance FromJSON Chapter where+ parseJSON = withObject "Chapter Entry" $ \v -> do+ let raw = v+ id <- v .: "id"+ timeBase <- v .: "time_base"+ startTime <- parseReadable =<< v .: "start_time"+ endTime <- parseReadable =<< v .: "end_time"+ tags <- parseTags =<< v .: "tags"+ return Chapter {..}
+ src/FFProbe/Data/Format.hs view
@@ -0,0 +1,45 @@+module FFProbe.Data.Format (Format (..)) where++import Data.Aeson.Types+import FFProbe.Data.Tags+import FFProbe.Data.Tags.Internal+import FFProbe.Internal (parseReadable)++data Format = Format+ { filename :: String,+ streamsCount :: Integer,+ programsCount :: Integer,+ streamGroupsCount :: Integer,+ formatName :: String,+ formatLongName :: String,+ startTime :: Float,+ -- | Duration in seconds+ duration :: Float,+ -- | Size of the file, in bytes+ size :: Integer,+ bitrate :: Integer,+ probeScore :: Integer,+ tags :: TagList,+ -- | The aeson object for the entire JSON received from ffprobe.+ raw :: Object+ }++instance HasTags Format where+ getTags = tags++instance FromJSON Format where+ parseJSON = withObject "Format" $ \v -> do+ let raw = v+ filename <- v .: "filename"+ streamsCount <- v .: "nb_streams"+ programsCount <- v .: "nb_programs"+ streamGroupsCount <- v .: "nb_stream_groups"+ formatName <- v .: "format_name"+ formatLongName <- v .: "format_long_name"+ startTime <- parseReadable =<< v .: "start_time"+ duration <- parseReadable =<< v .: "duration"+ size <- parseReadable =<< v .: "size"+ bitrate <- parseReadable =<< v .: "bit_rate"+ probeScore <- v .: "probe_score"+ tags <- parseTags =<< v .: "tags"+ return Format {..}
+ src/FFProbe/Data/Stream.hs view
@@ -0,0 +1,181 @@+module FFProbe.Data.Stream (+ Stream (..),+ StreamType (..),+ isVideoStream,+ isAudioStream,+ isSubtitleStream,+ isStreamOfType,+ StreamDisposition (..),+) where++import Control.Applicative ((<|>))+import Data.Aeson+import Data.Aeson.Types (Parser)+import Data.Text hiding (index)+import FFProbe.Data.Tags (HasTags (..), TagList)+import FFProbe.Data.Tags.Internal+import FFProbe.Internal+import Prelude hiding (id)++data StreamType+ = VideoStream+ | AudioStream+ | SubtitleStream+ | DataStream+ | Attachment+ | Other String+ deriving (Eq, Show)++instance FromJSON StreamType where+ parseJSON (String "video") = return VideoStream+ parseJSON (String "audio") = return AudioStream+ parseJSON (String "subtitle") = return SubtitleStream+ parseJSON (String "data") = return DataStream+ parseJSON (String "attachment") = return Attachment+ parseJSON (String s) = return $ Other (unpack s)+ parseJSON x = return $ Other (show x)++data StreamDisposition = StreamDisposition+ { isDefault :: Bool,+ isDub :: Bool,+ isOriginal :: Bool,+ isComment :: Bool,+ isLyrics :: Bool,+ isKaraoke :: Bool,+ isForced :: Bool,+ isHearingImpaired :: Bool,+ isVisualImpaired :: Bool,+ isCleanEffects :: Bool,+ isAttachedPic :: Bool,+ isTimedThumbnails :: Bool,+ isNonDiegetic :: Bool,+ isCaptions :: Bool,+ isDescriptions :: Bool,+ isMetadata :: Bool,+ isDependent :: Bool,+ isStillImage :: Bool+ }++isVideoStream :: Stream -> Bool+isVideoStream = isStreamOfType VideoStream++isAudioStream :: Stream -> Bool+isAudioStream = isStreamOfType AudioStream++isSubtitleStream :: Stream -> Bool+isSubtitleStream = isStreamOfType SubtitleStream++isStreamOfType :: StreamType -> Stream -> Bool+isStreamOfType stype stream = stype == streamType stream++instance FromJSON StreamDisposition where+ parseJSON = withObject "Disposition" $ \v -> do+ let getValue key = (parseDispositionValue =<< v .: key) <|> pure False+ isDefault <- getValue "default"+ isDub <- getValue "dub"+ isOriginal <- getValue "original"+ isComment <- getValue "comment"+ isLyrics <- getValue "lyrics"+ isKaraoke <- getValue "karaoke"+ isForced <- getValue "forced"+ isHearingImpaired <- getValue "hearing_impaired"+ isVisualImpaired <- getValue "visual_impaired"+ isCleanEffects <- getValue "clean_effects"+ isAttachedPic <- getValue "attached_pic"+ isTimedThumbnails <- getValue "attached_pic"+ isNonDiegetic <- getValue "non_diegetic"+ isCaptions <- getValue "captions"+ isDescriptions <- getValue "descriptions"+ isMetadata <- getValue "metadata"+ isDependent <- getValue "dependent"+ isStillImage <- getValue "still_image"+ return StreamDisposition {..}+ where+ parseDispositionValue :: Int -> Parser Bool+ parseDispositionValue 0 = return False+ parseDispositionValue 1 = return True+ parseDispositionValue n = fail $ "Expected 0 or 1. Got " ++ show n++data Stream = Stream+ { index :: Integer,+ codecName :: String,+ codecLongName :: String,+ codecType :: String,+ streamType :: StreamType,+ codecTagString :: String,+ -- | Example: "0x0000"+ codecTag :: String,+ rFrameRate :: String,+ averageFrameRate :: String,+ timeBase :: String,+ startPts :: Integer,+ startTime :: Float,+ -- | Duration of the stream, in seconds+ duration :: Maybe Float,+ bitRate :: Maybe Integer,+ bitsPerRawSample :: Maybe Integer,+ bitsPerSample :: Maybe Integer,+ framesCount :: Maybe Integer,+ tags :: TagList,+ disposition :: StreamDisposition,+ fieldOrder :: Maybe String,+ profile :: Maybe String,+ width :: Maybe Integer,+ height :: Maybe Integer,+ hasBFrames :: Maybe Integer,+ -- TODO RATIO+ sampleAspectRatio :: Maybe String,+ displayAspectRatio :: Maybe String,+ pixFmt :: Maybe String,+ level :: Maybe Integer,+ colorRange :: Maybe String,+ colorSpace :: Maybe String,+ sampleFmt :: Maybe String,+ sampleRate :: Maybe Integer,+ channels :: Maybe Integer,+ channelLayout :: Maybe String,+ -- | The aeson object for the entire JSON received from ffprobe.+ raw :: Object+ }++instance HasTags Stream where+ getTags = tags++instance FromJSON Stream where+ parseJSON = withObject "Stream" $ \o -> do+ let raw = o+ index <- o .: "index"+ codecName <- o .: "codec_name"+ codecLongName <- o .: "codec_long_name"+ codecType <- o .: "codec_type"+ streamType <- parseJSON (String $ pack codecType)+ codecTagString <- o .: "codec_tag_string"+ codecTag <- o .: "codec_tag"+ rFrameRate <- o .: "r_frame_rate"+ averageFrameRate <- o .: "avg_frame_rate"+ timeBase <- o .: "time_base"+ startPts <- o .: "start_pts"+ startTime <- parseReadable =<< o .: "start_time"+ duration <- parseOptionalValue =<< o .:? "duration"+ bitRate <- parseOptionalValue =<< o .:? "bit_rate"+ bitsPerRawSample <- parseOptionalValue =<< o .:? "bits_per_raw_sample"+ bitsPerSample <- o .:? "bits_per_sample"+ framesCount <- parseOptionalValue =<< o .:? "nb_frames"+ tags <- parseTags =<< o .: "tags"+ disposition <- o .: "disposition"+ fieldOrder <- o .:? "field_order"+ profile <- o .:? "profile"+ width <- o .:? "width"+ height <- o .:? "height"+ hasBFrames <- o .:? "has_b_frames"+ sampleAspectRatio <- o .:? "sample_aspect_ratio"+ displayAspectRatio <- o .:? "display_aspect_ratio"+ pixFmt <- o .:? "pix_fmt"+ level <- o .:? "level"+ colorRange <- o .:? "color_range"+ colorSpace <- o .:? "color_space"+ sampleFmt <- o .:? "sample_fmt"+ sampleRate <- parseOptionalValue =<< o .:? "sample_rate"+ channels <- o .:? "channels"+ channelLayout <- o .:? "channel_layout"+ return Stream {..}
+ src/FFProbe/Data/Tags.hs view
@@ -0,0 +1,29 @@+module FFProbe.Data.Tags (TagList, TagValue (..), HasTags (..), lookupTag) where++import Data.Aeson+import Data.Scientific (floatingOrInteger)+import Data.Text (unpack)++type TagList = [(String, TagValue)]++data TagValue+ = StringTag String+ | IntTag Integer+ | FloatTag Float+ | -- | If the constructor is 'Other', the String is a JSON representation of the value+ Other String+ | Null+ deriving (Show, Eq)++instance FromJSON TagValue where+ parseJSON (String v) = return $ StringTag $ unpack v+ parseJSON (Number v) = return $ either FloatTag IntTag (floatingOrInteger v)+ parseJSON Data.Aeson.Null = return FFProbe.Data.Tags.Null+ parseJSON x = return $ Other $ show x++class HasTags a where+ getTags :: a -> TagList++-- | Lookup a tag in a TagList, using a key+lookupTag :: (HasTags a) => String -> a -> Maybe TagValue+lookupTag key obj = lookup key (getTags obj)
+ src/FFProbe/Data/Tags/Internal.hs view
@@ -0,0 +1,16 @@+module FFProbe.Data.Tags.Internal (parseTags) where++import Data.Aeson+import Data.Aeson.Key (toString)+import Data.Aeson.KeyMap (toList)+import Data.Aeson.Types (Parser)+import Data.Bifunctor (Bifunctor (bimap))+import Data.Text (unpack)+import FFProbe.Data.Tags++parseTags :: Value -> Parser TagList+parseTags = withObject "Tags" $ \kvmap -> do+ pure $ map (bimap toString parseTag) (toList kvmap)+ where+ parseTag (String v) = StringTag $ unpack v+ parseTag x = Other $ show x
+ src/FFProbe/Exec.hs view
@@ -0,0 +1,24 @@+module FFProbe.Exec (execFFProbe) where++import Control.Exception (IOException, try)+import Data.Bifunctor (first)+import Data.Functor ((<&>))+import System.Process (proc, readCreateProcess)++-- | Runs ffprobes, returns the output of the command+execFFProbe :: String -> IO (Either String String)+execFFProbe path = try_ (readCreateProcess process input) <&> first show+ where+ try_ :: IO String -> IO (Either IOException String)+ try_ = try+ input = ""+ process = proc "ffprobe" (ffprobeArgs ++ [path])+ ffprobeArgs =+ [ "-v",+ "quiet",+ "-print_format",+ "json",+ "-show_format",+ "-show_streams",+ "-show_chapters"+ ]
+ src/FFProbe/Internal.hs view
@@ -0,0 +1,15 @@+module FFProbe.Internal (parseReadable, parseOptionalValue) where++import Data.Aeson.Types (Parser)+import Text.Read (readMaybe)++-- | applies `readMaybe` to a value, and turns the result into a Parser+parseReadable :: (Read a) => String -> Parser a+parseReadable n = case readMaybe n of+ Nothing -> fail "Failed to read value"+ Just x -> pure x++-- | applies `readMaybe` to a value, and turns the result into a Parser+parseOptionalValue :: (Read a) => Maybe String -> Parser (Maybe a)+parseOptionalValue Nothing = pure Nothing+parseOptionalValue (Just v) = Just <$> parseReadable v
+ test/FFProbe/Data/TestChapter.hs view
@@ -0,0 +1,22 @@+module FFProbe.Data.TestChapter (specs) where++import Data.Aeson+import FFProbe.Data.Chapter+import Test.Hspec+import Utils+import Prelude hiding (id)++specs :: Spec+specs = describe "Chapter Parsing" $ do+ it "Should Parse the correct values" $ do+ rawJson <- getAssetContent "chapter.json"+ shouldBeRight+ (eitherDecodeStrict rawJson)+ ( \chapter -> do+ FFProbe.Data.Chapter.id chapter `shouldBe` 8+ timeBase chapter `shouldBe` "1/1000000000"+ startTime chapter `shouldBe` 0.0+ endTime chapter `shouldBe` 117.56+ length (tags chapter) `shouldBe` 1+ title chapter `shouldBe` Just "Chapter 01"+ )
+ test/FFProbe/Data/TestFormat.hs view
@@ -0,0 +1,28 @@+module FFProbe.Data.TestFormat (specs) where++import Data.Aeson+import FFProbe.Data.Format+import FFProbe.Data.Tags+import Test.Hspec+import Utils+import Prelude hiding (id)++specs :: Spec+specs = describe "Format Parsing" $ do+ it "Should Parse the correct values" $ do+ rawJson <- getAssetContent "format.json"+ shouldBeRight+ (eitherDecodeStrict rawJson)+ ( \format -> do+ filename format `shouldBe` "A.mkv"+ streamsCount format `shouldBe` 5+ programsCount format `shouldBe` 0+ streamGroupsCount format `shouldBe` 0+ formatName format `shouldBe` "matroska,webm"+ formatLongName format `shouldBe` "Matroska / WebM"+ startTime format `shouldBe` 0.0+ duration format `shouldBe` 5272.96+ bitrate format `shouldBe` 5406763+ length (tags format) `shouldBe` 1+ lookupTag "ENCODER" format `shouldBe` Just (StringTag "Lavf60.16.100")+ )
+ test/FFProbe/Data/TestStream.hs view
@@ -0,0 +1,94 @@+module FFProbe.Data.TestStream (specs) where++import Data.Aeson+import FFProbe.Data.Stream+import FFProbe.Data.Tags (TagValue (StringTag), lookupTag)+import Test.Hspec+import Utils+import Prelude hiding (id)++specs :: Spec+specs = describe "Stream Parsing" $ do+ it "Audio Stream" $ do+ rawJson <- getAssetContent "stream-audio.json"+ shouldBeRight+ (eitherDecodeStrict rawJson)+ ( \stream -> do+ index stream `shouldBe` 1+ codecName stream `shouldBe` "pcm_s16le"+ codecLongName stream `shouldBe` "PCM signed 16-bit little-endian"+ codecType stream `shouldBe` "audio"+ streamType stream `shouldBe` AudioStream+ codecTagString stream `shouldBe` "[0][0][0][0]"+ codecTag stream `shouldBe` "0x0000"+ sampleFmt stream `shouldBe` Just "s16"+ sampleRate stream `shouldBe` Just 48000+ channels stream `shouldBe` Just 2+ bitsPerSample stream `shouldBe` Just 16+ rFrameRate stream `shouldBe` "0/0"+ averageFrameRate stream `shouldBe` "0/0"+ rFrameRate stream `shouldBe` "0/0"+ timeBase stream `shouldBe` "1/1000"+ startPts stream `shouldBe` 0+ startTime stream `shouldBe` 0.0+ bitRate stream `shouldBe` Just 1536000+ isDefault (disposition stream) `shouldBe` True+ isForced (disposition stream) `shouldBe` False+ isNonDiegetic (disposition stream) `shouldBe` False+ lookupTag "language" stream `shouldBe` Just (StringTag "eng")+ )+ it "Video Stream" $ do+ rawJson <- getAssetContent "stream-video.json"+ shouldBeRight+ (eitherDecodeStrict rawJson)+ ( \stream -> do+ index stream `shouldBe` 0+ codecName stream `shouldBe` "h264"+ codecLongName stream `shouldBe` "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"+ codecType stream `shouldBe` "video"+ streamType stream `shouldBe` VideoStream+ codecTagString stream `shouldBe` "[0][0][0][0]"+ codecTag stream `shouldBe` "0x0000"+ width stream `shouldBe` Just 720+ height stream `shouldBe` Just 576+ hasBFrames stream `shouldBe` Just 2+ sampleAspectRatio stream `shouldBe` Just "64:45"+ displayAspectRatio stream `shouldBe` Just "16:9"+ pixFmt stream `shouldBe` Just "yuv420p"+ level stream `shouldBe` Just 30+ colorRange stream `shouldBe` Just "tv"+ fieldOrder stream `shouldBe` Just "tb"+ bitsPerSample stream `shouldBe` Nothing+ rFrameRate stream `shouldBe` "25/1"+ averageFrameRate stream `shouldBe` "25/1"+ timeBase stream `shouldBe` "1/1000"+ startPts stream `shouldBe` 0+ startTime stream `shouldBe` 0.0+ bitRate stream `shouldBe` Nothing+ bitsPerRawSample stream `shouldBe` Just 8+ isDefault (disposition stream) `shouldBe` False+ lookupTag "ENCODER" stream `shouldBe` Just (StringTag "Lavc60.31.102 libx264")+ )+ it "Subtitle Stream" $ do+ rawJson <- getAssetContent "stream-subtitle.json"+ shouldBeRight+ (eitherDecodeStrict rawJson)+ ( \stream -> do+ index stream `shouldBe` 3+ codecName stream `shouldBe` "dvd_subtitle"+ codecLongName stream `shouldBe` "DVD subtitles"+ codecType stream `shouldBe` "subtitle"+ streamType stream `shouldBe` SubtitleStream+ codecTagString stream `shouldBe` "[0][0][0][0]"+ codecTag stream `shouldBe` "0x0000"+ rFrameRate stream `shouldBe` "0/0"+ averageFrameRate stream `shouldBe` "0/0"+ rFrameRate stream `shouldBe` "0/0"+ timeBase stream `shouldBe` "1/1000"+ startPts stream `shouldBe` 0+ startTime stream `shouldBe` 0.0+ duration stream `shouldBe` Just 5272.96+ isDefault (disposition stream) `shouldBe` True+ isForced (disposition stream) `shouldBe` False+ lookupTag "language" stream `shouldBe` Just (StringTag "eng")+ )
+ test/FFProbe/TestFFProbe.hs view
@@ -0,0 +1,27 @@+module FFProbe.TestFFProbe (specs) where++import FFProbe+import FFProbe.Data.Chapter+import FFProbe.Data.Stream+import Test.Hspec+import Utils (shouldBeRight)++specs :: Spec+specs = describe "Running ffprobe" $ do+ it "Should Run ffprobe and give the correct values" $ do+ res <- ffprobe "test/assets/test.mp4"+ shouldBeRight+ res+ ( \ffprobeData -> do+ -- Testing streams+ length (streams ffprobeData) `shouldBe` 3+ let videoStream = filter isVideoStream (streams ffprobeData)+ length videoStream `shouldBe` 1+ let audioStream = filter isAudioStream (streams ffprobeData)+ length audioStream `shouldBe` 1+ -- Testing Chapters+ length (chapters ffprobeData) `shouldBe` 3+ title (head (chapters ffprobeData)) `shouldBe` Just "Start"+ title (chapters ffprobeData !! 1) `shouldBe` Just "Middle"+ title (chapters ffprobeData !! 2) `shouldBe` Just "End"+ )
+ test/Spec.hs view
@@ -0,0 +1,12 @@+import qualified FFProbe.Data.TestChapter as TestChapter+import qualified FFProbe.Data.TestFormat as TestFormat+import qualified FFProbe.Data.TestStream as TestStream+import qualified FFProbe.TestFFProbe as TestFFProbe+import Test.Hspec++main :: IO ()+main = hspec $ do+ TestChapter.specs+ TestFormat.specs+ TestStream.specs+ TestFFProbe.specs
+ test/Utils.hs view
@@ -0,0 +1,11 @@+module Utils where++import Data.ByteString+import Test.Hspec.Expectations++getAssetContent :: String -> IO ByteString+getAssetContent fp = Data.ByteString.readFile $ "test/assets/" <> fp++shouldBeRight :: Either String a -> (a -> Expectation) -> Expectation+shouldBeRight (Left err) _ = expectationFailure err+shouldBeRight (Right value) f = f value